toolManager: fixed annotate tool issues and added Text Marker Tool (importing callback functions from Nucleus) and using Metadata classes for OHIF Viewer

This commit is contained in:
Eloízio Salgado 2017-01-18 13:59:03 -02:00 committed by Emanuel F. Oliveira
parent 014dc7730f
commit a8626d02c0
24 changed files with 516 additions and 242 deletions

View File

@ -55,9 +55,9 @@ Template.viewer.onCreated(() => {
ViewerData[contentId].studyInstanceUids = []; ViewerData[contentId].studyInstanceUids = [];
instance.data.studies.forEach(study => { instance.data.studies.forEach(study => {
study.selected = true; study.selected = true;
study.displaySets = OHIF.viewerbase.createStacks(study); // study.displaySets = OHIF.viewerbase.createStacks(study);
// const studyMetadata = new OHIF.metadata.StudyMetadata(study); const studyMetadata = new OHIF.metadata.StudyMetadata(study);
// study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
OHIF.viewer.Studies.insert(study); OHIF.viewer.Studies.insert(study);
ViewerData[contentId].studyInstanceUids.push(study.studyInstanceUid); ViewerData[contentId].studyInstanceUids.push(study.studyInstanceUid);
}); });

View File

@ -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 // 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. // 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. // 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],...) // HuffmanValue(table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...)
// ): // ):
// return: Huffman Value of table // return: Huffman Value of table
// 0xFF?? if it receives a MARKER // 0xFF?? if it receives a MARKER
// Parameter: table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...) // Parameter: table HuffTab[x][y] (ex) HuffmanValue(HuffTab[1][0],...)
// temp temp storage for remainded bits // temp temp storage for remainded bits
// index index to bit of temp // index index to bit of temp
// in FILE pointer // in FILE pointer
// Effect: // Effect:
// temp store new remainded bits // temp store new remainded bits
// index change to new index // index change to new index
// in change to new position // in change to new position
// NOTE: // NOTE:
// Initial by temp=0; index=0; // Initial by temp=0; index=0;
// NOTE: (explain temp and index) // NOTE: (explain temp and index)
// temp: is always in the form at calling time or returning time // temp: is always in the form at calling time or returning time
// | byte 4 | byte 3 | byte 2 | byte 1 | // | byte 4 | byte 3 | byte 2 | byte 1 |
// | 0 | 0 | 00000000 | 00000??? | if not a MARKER // | 0 | 0 | 00000000 | 00000??? | if not a MARKER
// ^index=3 (from 0 to 15) // ^index=3 (from 0 to 15)
// 321 // 321
// NOTE (marker and marker_index): // NOTE (marker and marker_index):
// If get a MARKER from 'in', marker=the low-byte of the MARKER // If get a MARKER from 'in', marker=the low-byte of the MARKER
// and marker_index=9 // and marker_index=9
// If marker_index=9 then index is always > 8, or HuffmanValue() // If marker_index=9 then index is always > 8, or HuffmanValue()
// will not be called // will not be called
jpeg.lossless.Decoder.prototype.getHuffmanValue = function (table, temp, index) { jpeg.lossless.Decoder.prototype.getHuffmanValue = function (table, temp, index) {
/*jslint bitwise: true */ /*jslint bitwise: true */
@ -3302,13 +3302,13 @@ var JpegImage = (function jpegImage() {
// Build_HuffTab() // Build_HuffTab()
// Parameter: t table ID // Parameter: t table ID
// c table class ( 0 for DC, 1 for AC ) // c table class ( 0 for DC, 1 for AC )
// L[i] # of codewords which length is i // L[i] # of codewords which length is i
// V[i][j] Huffman Value (length=i) // V[i][j] Huffman Value (length=i)
// Effect: // Effect:
// build up HuffTab[t][c] using L and V. // build up HuffTab[t][c] using L and V.
jpeg.lossless.HuffmanTable.prototype.buildHuffTable = function(tab, L, V) { jpeg.lossless.HuffmanTable.prototype.buildHuffTable = function(tab, L, V) {
/*jslint bitwise: true */ /*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 // https://groups.google.com/forum/#!searchin/comp.protocols.dicom/Modality$20LUT$20XA/comp.protocols.dicom/UBxhOZ2anJ0/D0R_QP8V2wIJ
var sopClassUid = dataSet.string('x00080016'); var sopClassUid = dataSet.string('x00080016');
return sopClassUid !== '1.2.840.10008.5.1.4.1.1.12.1' && // XA 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) { function makeGrayscaleImage(imageId, dataSet, frame, sharedCacheKey) {

View File

@ -404,7 +404,6 @@ HP.ProtocolEngine = class ProtocolEngine {
} }
// @TypeSafeStudies // @TypeSafeStudies
var alreadyLoaded = OHIF.viewer.Studies.findBy({ var alreadyLoaded = OHIF.viewer.Studies.findBy({
studyInstanceUid: priorStudy.studyInstanceUid studyInstanceUid: priorStudy.studyInstanceUid
}); });

View File

@ -30,7 +30,7 @@ OHIF.lesiontracker.openNewTabWithTimepoint = (timepointId, title) => {
// Update the ViewerData global object // Update the ViewerData global object
ViewerData[contentId] = { ViewerData[contentId] = {
title: title, title: title,
contentid: contentId, contentId: contentId,
studyInstanceUids: data.studyInstanceUids, studyInstanceUids: data.studyInstanceUids,
timepointIds: data.timepointIds, timepointIds: data.timepointIds,
currentTimepointId: timepointId currentTimepointId: timepointId

View File

@ -42,18 +42,18 @@ function dblClickOnStudy(data) {
* @param title The title to be used for the tab heading * @param title The title to be used for the tab heading
*/ */
function open(studyInstanceUid, title) { function open(studyInstanceUid, title) {
const contentid = 'viewerTab'; const contentId = 'viewerTab';
ViewerData = window.ViewerData || ViewerData; ViewerData = window.ViewerData || ViewerData;
// Update the ViewerData global object // Update the ViewerData global object
ViewerData[contentid] = { ViewerData[contentId] = {
title: title, title: title,
contentid: contentid, contentId: contentId,
isUnassociatedStudy: true, isUnassociatedStudy: true,
studyInstanceUids: [studyInstanceUid] studyInstanceUids: [studyInstanceUid]
}; };
// Switch to the new tab // Switch to the new tab
switchToTab(contentid); switchToTab(contentId);
} }

View File

@ -135,17 +135,17 @@ function viewStudies() {
const title = selectedStudies[0].patientName; const title = selectedStudies[0].patientName;
const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid); const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid);
const contentid = 'viewerTab'; const contentId = 'viewerTab';
ViewerData = window.ViewerData || ViewerData; ViewerData = window.ViewerData || ViewerData;
// Update the ViewerData global object // Update the ViewerData global object
ViewerData[contentid] = { ViewerData[contentId] = {
title: title, title: title,
contentid: contentid, contentId: contentId,
studyInstanceUids: studyInstanceUids studyInstanceUids: studyInstanceUids
}; };
// Switch to the new tab // Switch to the new tab
switchToTab(contentid); switchToTab(contentId);
} }

View File

@ -32,7 +32,6 @@ function findAndRenderDisplaySet(displaySets, viewportIndex, studyInstanceUid, s
function renderIntoViewport(viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback) { function renderIntoViewport(viewportIndex, studyInstanceUid, seriesInstanceUid, sopInstanceUid, renderedCallback) {
// @TypeSafeStudies // @TypeSafeStudies
// First, check if we already have this study loaded // First, check if we already have this study loaded
const alreadyLoadedStudy = OHIF.viewer.Studies.findBy({ studyInstanceUid }); const alreadyLoadedStudy = OHIF.viewer.Studies.findBy({ studyInstanceUid });

View File

@ -11,16 +11,16 @@ openNewTab = function(studyInstanceUid, title) {
// Generate a unique ID to represent this tab // Generate a unique ID to represent this tab
// We can't just use the Mongo entry ID because // We can't just use the Mongo entry ID because
// then it will change after hot-reloading. // then it will change after hot-reloading.
var contentid = Random.id(); var contentId = Random.id();
// Update the ViewerData global object // Update the ViewerData global object
ViewerData = window.ViewerData || ViewerData; ViewerData = window.ViewerData || ViewerData;
ViewerData[contentid] = { ViewerData[contentId] = {
title: title, title: title,
contentid: contentid, contentId: contentId,
studyInstanceUids: [studyInstanceUid] studyInstanceUids: [studyInstanceUid]
}; };
// Switch to the new tab // Switch to the new tab
switchToTab(contentid); switchToTab(contentId);
}; };

View File

@ -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'; 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 // If we are switching to the StudyList tab, reset any CSS styles
// that have been applied to prevent scrolling in the Viewer. // that have been applied to prevent scrolling in the Viewer.
// Then stop here, since nothing needs to be re-rendered. // Then stop here, since nothing needs to be re-rendered.
var container; let container;
if (contentId === 'studylistTab') { if (contentId === 'studylistTab') {
container = $('.tab-content').find('#studylistTab').get(0); container = $('.tab-content').find('#studylistTab').get(0);
@ -41,7 +46,7 @@ switchToTab = function(contentId) {
return; return;
} }
var studylistContainer = document.createElement('div'); const studylistContainer = document.createElement('div');
studylistContainer.classList.add('studylistContainer'); studylistContainer.classList.add('studylistContainer');
container.appendChild(studylistContainer); container.appendChild(studylistContainer);
@ -61,14 +66,14 @@ switchToTab = function(contentId) {
return; return;
} }
var container = $('.tab-content').find('#viewerTab').get(0); container = $('.tab-content').find('#viewerTab').get(0);
container.innerHTML = ''; container.innerHTML = '';
// Use Blaze to render the Loading Template into the container // Use Blaze to render the Loading Template into the container
// viewStudiesInTab will clear this container // viewStudiesInTab will clear this container
Blaze.renderWithData(Template.loadingText, {}, container); Blaze.renderWithData(Template.loadingText, {}, container);
var studies = ViewerData[contentId].studies; const studies = ViewerData[contentId].studies;
if (studies) { if (studies) {
// ViewerData already has the meta data (in cases when studylist is launched externally) // ViewerData already has the meta data (in cases when studylist is launched externally)
@ -76,7 +81,7 @@ switchToTab = function(contentId) {
} else { } else {
// Use the stored ViewerData global object to retrieve the studyInstanceUid // Use the stored ViewerData global object to retrieve the studyInstanceUid
// related to this tab // related to this tab
var studyInstanceUids = ViewerData[contentId].studyInstanceUids; const studyInstanceUids = ViewerData[contentId].studyInstanceUids;
// Attempt to retrieve the meta data (it might be cached) // Attempt to retrieve the meta data (it might be cached)
OHIF.studylist.getStudiesMetadata(studyInstanceUids, function(studies) { 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 // Tab closed while study data was being retrieved, stop here
if (!ViewerData[contentId]) { if (!ViewerData[contentId]) {
OHIF.log.warn('Tab closed while study data was being retrieved'); 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 // Once we have the study data, store it in a structure with
// any other saved data about this tab (e.g. layout structure) // 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; data.studies = studies;
// TODO: check if this is necessary. Since the typo bug
// was fixed (it was contentid instead of contentId)
data.contentId = contentId; data.contentId = contentId;
if (ViewerData[contentId].studies && ViewerData[contentId].studies.length) { 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 // Add additional metadata to our study from the studylist
data.studies.forEach(function(study) { data.studies.forEach(study => {
var studylistStudy = StudyListStudies.findOne({ const studylistStudy = StudyListStudies.findOne({
studyInstanceUid: study.studyInstanceUid studyInstanceUid: study.studyInstanceUid
}); });
@ -117,10 +125,10 @@ function viewStudiesInTab(contentId, studies) {
// Get tab content container given the contentId string // Get tab content container given the contentId string
// If no such container exists, stop here because something is wrong // 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 // 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'); viewerContainer.classList.add('viewerContainer');
container.innerHTML = ''; container.innerHTML = '';
container.appendChild(viewerContainer); container.appendChild(viewerContainer);
@ -129,7 +137,7 @@ function viewStudiesInTab(contentId, studies) {
Blaze.renderWithData(Template.viewer, data, viewerContainer); Blaze.renderWithData(Template.viewer, data, viewerContainer);
// Retrieve the DOM element of the viewer // 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 // If it is present in the DOM (it should be), then apply
// styles to prevent page scrolling and overscrolling on mobile devices // styles to prevent page scrolling and overscrolling on mobile devices
@ -142,4 +150,4 @@ function viewStudiesInTab(contentId, studies) {
// Prevent overscroll on mobile devices // Prevent overscroll on mobile devices
document.body.style.position = 'fixed'; document.body.style.position = 'fixed';
} }
} };

View File

@ -127,7 +127,9 @@ function resultDataToStudyMetadata(studyInstanceUid, resultData) {
derivationDescription: instance[0x00282111], derivationDescription: instance[0x00282111],
lossyImageCompressionRatio: instance[0x00282112], lossyImageCompressionRatio: instance[0x00282112],
lossyImageCompressionMethod: instance[0x00282114], lossyImageCompressionMethod: instance[0x00282114],
spacingBetweenSlices: instance[0x00180088] spacingBetweenSlices: instance[0x00180088],
echoNumber: instance[0x00180086],
contrastBolusAgent: instance[0x00180010]
}; };
// Retrieve the actual data over WADO-URI // Retrieve the actual data over WADO-URI

View File

@ -100,7 +100,9 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
numberOfFrames: parseFloat(remoteGetValue(instance['0028,0008'])), numberOfFrames: parseFloat(remoteGetValue(instance['0028,0008'])),
frameIncrementPointer: remoteGetValue(instance['0028,0009']), frameIncrementPointer: remoteGetValue(instance['0028,0009']),
frameTime: parseFloat(remoteGetValue(instance['0018,1063'])), 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; var iid = instance['xxxx,0001'].Value;

View File

@ -118,6 +118,8 @@ function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
derivationDescription: DICOMWeb.getString(instance['00282111']), derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']), lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']), lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010'])
}; };
if (server.imageRendering === 'wadouri') { if (server.imageRendering === 'wadouri') {

View File

@ -29,7 +29,6 @@ Template.studySeriesQuickSwitch.onCreated(() => {
const viewportData = instance.getViewportData(viewportIndex); const viewportData = instance.getViewportData(viewportIndex);
// @TypeSafeStudies // @TypeSafeStudies
if (viewportData) { if (viewportData) {
// Finds the current study and return it // Finds the current study and return it
instance.study = OHIF.viewer.Studies.findBy({ instance.study = OHIF.viewer.Studies.findBy({

View File

@ -41,7 +41,6 @@ Template.studyTimepoint.events({
let $studiesTarget = instance.$('.studyTimepoint'); let $studiesTarget = instance.$('.studyTimepoint');
// @TypeSafeStudies // @TypeSafeStudies
if (changed.isQuickSwitch) { if (changed.isQuickSwitch) {
// Changes the current quick switch study // Changes the current quick switch study
const study = OHIF.viewer.Studies.findBy({ const study = OHIF.viewer.Studies.findBy({

View File

@ -139,6 +139,14 @@ Viewerbase.sortingManager = sortingManager;
import { crosshairsSynchronizers } from './lib/crosshairsSynchronizers'; import { crosshairsSynchronizers } from './lib/crosshairsSynchronizers';
Viewerbase.crosshairsSynchronizers = crosshairsSynchronizers; Viewerbase.crosshairsSynchronizers = crosshairsSynchronizers;
// annotateTextUtils.*
import { annotateTextUtils } from './lib/annotateTextUtils';
Viewerbase.annotateTextUtils = annotateTextUtils;
// textMarkerUtils.*
import { textMarkerUtils } from './lib/textMarkerUtils';
Viewerbase.textMarkerUtils = textMarkerUtils;
// createStacks.* // createStacks.*
import { createStacks } from './lib/createStacks'; import { createStacks } from './lib/createStacks';
Viewerbase.createStacks = createStacks; Viewerbase.createStacks = createStacks;

View File

@ -17,41 +17,50 @@ const stackUpdatedCallbacks = [];
* @return {Array} Array with image IDs * @return {Array} Array with image IDs
*/ */
function createAndAddStack(stackMap, study, displaySet) { function createAndAddStack(stackMap, study, displaySet) {
const numImages = displaySet.images.length; const numImages = displaySet.images.length;
const imageIds = []; const imageIds = [];
let imageId; let imageId;
displaySet.images.forEach((image, imageIndex) => { displaySet.images.forEach((image, imageIndex) => {
const metaData = { const metaData = {
instance: image, instance: image,
series: displaySet, // TODO: Check this series: displaySet, // TODO: Check this
study: study, study: study,
numImages: numImages, numImages: numImages,
imageIndex: imageIndex + 1 imageIndex: imageIndex + 1
}; };
const numberOfFrames = image.numberOfFrames; const numberOfFrames = image.numberOfFrames;
if (numberOfFrames > 1) { if (numberOfFrames > 1) {
OHIF.log.info('Multiframe image detected'); OHIF.log.info('Multiframe image detected');
for (let i = 0; i < numberOfFrames; i++) { for (let i = 0; i < numberOfFrames; i++) {
metaData.frame = i; metaData.frame = i;
imageId = getImageId(image, i); imageId = getImageId(image, i);
imageIds.push(imageId); imageIds.push(imageId);
addMetaData(imageId, metaData); addMetaData(imageId, metaData);
} }
} }
else { else {
imageId = getImageId(image); imageId = getImageId(image);
imageIds.push(imageId); imageIds.push(imageId);
addMetaData(imageId, metaData); 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 = { configuration = {
createAndAddStack: createAndAddStack createAndAddStack: createAndAddStack
}; };
/** /**
@ -61,64 +70,64 @@ configuration = {
* clearStacks and makeAndAddStack should not used outside of loadStudy and removeStudies. * clearStacks and makeAndAddStack should not used outside of loadStudy and removeStudies.
*/ */
const StackManager = { const StackManager = {
/** /**
* Removes all current stacks * Removes all current stacks
*/ */
clearStacks() { clearStacks() {
stackMap = {}; stackMap = {};
}, },
/** /**
* Create a stack from an image set, as well as add in the metadata on a per image bases. * 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 study The study who's metadata will be added
* @param displaySet The set of images to make the stack from * @param displaySet The set of images to make the stack from
* @return {Array} Array with image IDs * @return {Array} Array with image IDs
*/ */
makeAndAddStack(study, displaySet) { makeAndAddStack(study, displaySet) {
return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks); return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks);
}, },
/** /**
* Find a stack from the currently created stacks. * Find a stack from the currently created stacks.
* @param displaySetInstanceUid The UID of the stack to find. * @param displaySetInstanceUid The UID of the stack to find.
* @returns {*} undefined if not found, otherwise the stack object is returned. * @returns {*} undefined if not found, otherwise the stack object is returned.
*/ */
findStack(displaySetInstanceUid) { findStack(displaySetInstanceUid) {
return stackMap[displaySetInstanceUid]; return stackMap[displaySetInstanceUid];
}, },
/** /**
* Gets the underlying map of displaySetInstanceUid to stack object. * Gets the underlying map of displaySetInstanceUid to stack object.
* WARNING: Do not change this object. It directly affects the manager. * WARNING: Do not change this object. It directly affects the manager.
* @returns {{}} map of displaySetInstanceUid -> stack. * @returns {{}} map of displaySetInstanceUid -> stack.
*/ */
getAllStacks() { getAllStacks() {
return stackMap; return stackMap;
}, },
/** /**
* Adds in a callback to be called on a stack being added / updated. * Adds in a callback to be called on a stack being added / updated.
* @param callback must accept at minimum one argument, * @param callback must accept at minimum one argument,
* which is the stack that was added / updated. * which is the stack that was added / updated.
*/ */
addStackUpdatedCallback(callback) { addStackUpdatedCallback(callback) {
if (typeof callback !== 'function') { if (typeof callback !== 'function') {
throw new OHIFError('callback must be provided as a 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 }; export { StackManager };

View File

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

View File

@ -88,13 +88,18 @@ export class LayoutManager {
// Generate the additional data based on the appendix // Generate the additional data based on the appendix
const additionalData = []; const additionalData = [];
appendix.forEach((displaySet, index) => { appendix.forEach((displaySet, index) => {
additionalData.push({ const { images, studyInstanceUid, seriesInstanceUid, displaySetInstanceUid } = displaySet;
viewportIndex: currentViewportIndex + index, const sopInstanceUid = images[0] && images[0].getSOPInstanceUID ? images[0].getSOPInstanceUID() : '';
studyInstanceUid: displaySet.studyInstanceUid, const viewportIndex = currentViewportIndex + index;
seriesInstanceUid: displaySet.seriesInstanceUid, const data = {
displaySetInstanceUid: displaySet.displaySetInstanceUid, viewportIndex,
sopInstanceUid: displaySet.images[0].sopInstanceUid studyInstanceUid,
}); seriesInstanceUid,
displaySetInstanceUid,
sopInstanceUid
};
additionalData.push(data);
}); });
// Append the additional data with the viewport data // Append the additional data with the viewport data

View File

@ -1,40 +1,86 @@
import { ImageSet } from './classes/ImageSet'; import { ImageSet } from './classes/ImageSet';
import { isImage } from './isImage'; 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 * Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Browser must represent * The series that appear in the Study Metadata must represent
* imaging modalities. * imaging modalities.
* *
* Furthermore, for drag/drop functionality, * Furthermore, for drag/drop functionality,
* it is easiest if the stack objects also contain information about * it is easiest if the stack objects also contain information about
* which study they are linked to. * which study they are linked to.
* *
* @param study The study instance to be used * @param study The study instance metadata to be used
* @returns {Array} An array of series to be placed in the Study Browser * @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 // Define an empty array of display sets
var displaySets = []; const displaySets = [];
if (!study || !study.seriesList) { if (!study || !study.getSeriesCount()) {
return displaySets; return displaySets;
} }
study.seriesList.forEach(series => { // Loop through the series (SeriesMetadata)
study.forEachSeries(series => {
// If the series has no instances, skip it // If the series has no instances, skip it
if (!series.instances) { if (!series.getInstanceCount()) {
return; 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 // Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each // into their own specific display sets. Place the rest of each
// series into another display set. // series into another display set.
let stackableInstances = []; const stackableInstances = [];
series.instances.forEach(instance => { series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid or rows // All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
if (!isImage(instance.sopClassUid) && !instance.rows) { if (!isImage(instance.getRawValue('x00080016')) && !instance.getRawValue('x00280010')) {
return; return;
} }
@ -63,8 +109,8 @@ function createStacks(study) {
}); });
if (stackableInstances.length) { if (stackableInstances.length) {
let displaySet = makeDisplaySet(series, stackableInstances); const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute('studyInstanceUid', study.studyInstanceUid); displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
displaySets.push(displaySet); displaySets.push(displaySet);
} }
}); });
@ -72,47 +118,6 @@ function createStacks(study) {
return displaySets; 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"... * Expose "createStacks"...
*/ */

View File

@ -7,7 +7,13 @@ import { getWADORSImageId } from './getWADORSImageId';
* @param instance * @param instance
* @returns {string} The imageId to be used by Cornerstone * @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) { if (!instance) {
return; return;
} }

View File

@ -4,28 +4,30 @@ import { OHIF } from 'meteor/ohif:core';
/** /**
* Obtain an imageId for Cornerstone based on the WADO-RS scheme * 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 * @returns {string} The imageId to be used by Cornerstone
*/ */
export function getWADORSImageId(instance) { export function getWADORSImageId(instanceMetada) {
var columnPixelSpacing = 1.0; const instance = instanceMetada.getData();
var rowPixelSpacing = 1.0; let columnPixelSpacing = 1.0;
let rowPixelSpacing = 1.0;
if (instance.pixelSpacing) { if (instance.pixelSpacing) {
var split = instance.pixelSpacing.split('\\'); const split = instance.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]); rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]); columnPixelSpacing = parseFloat(split[1]);
} }
var windowWidth; let windowWidth;
var windowCenter; let windowCenter;
if (instance.windowWidth && instance.windowCenter) { if (instance.windowWidth && instance.windowCenter) {
windowWidth = parseFloat(instance.windowWidth.split('\\')[0]); windowWidth = parseFloat(instance.windowWidth.split('\\')[0]);
windowCenter = parseFloat(instance.windowCenter.split('\\')[0]); windowCenter = parseFloat(instance.windowCenter.split('\\')[0]);
} }
var image = { const image = {
uri: Meteor.absoluteUrl(instance.wadorsuri), uri: Meteor.absoluteUrl(instance.wadorsuri),
//imageId : '', //imageId : '',
//minPixelValue : 0, //minPixelValue : 0,
@ -56,7 +58,7 @@ export function getWADORSImageId(instance) {
instance: instance instance: instance
}; };
var imageId = cornerstoneWADOImageLoader.imageManager.add(image); const imageId = cornerstoneWADOImageLoader.imageManager.add(image);
OHIF.log.info('WADO-RS ImageID: ' + imageId); OHIF.log.info('WADO-RS ImageID: ' + imageId);
return imageId; return imageId;

View File

@ -1,7 +1,7 @@
import { getSortedData } from './getSortedData'; import { createStacks } from './createStacks';
const getDisplaySets = (studyMetadata, seriesNumber, iteratorFunction) => { const getDisplaySets = (studyMetadata, seriesNumber, iteratorFunction) => {
const iteratorFn = typeof iteratorFunction !== 'function' ? getSortedData : iteratorFunction; const iteratorFn = typeof iteratorFunction !== 'function' ? createStacks : iteratorFunction;
return iteratorFn(studyMetadata, seriesNumber); return iteratorFn(studyMetadata, seriesNumber);
}; };

View File

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

View File

@ -4,6 +4,8 @@ import { OHIF } from 'meteor/ohif:core';
import { getFrameOfReferenceUID } from './getFrameOfReferenceUID'; import { getFrameOfReferenceUID } from './getFrameOfReferenceUID';
import { updateCrosshairsSynchronizer } from './updateCrosshairsSynchronizer'; import { updateCrosshairsSynchronizer } from './updateCrosshairsSynchronizer';
import { crosshairsSynchronizers } from './crosshairsSynchronizers'; import { crosshairsSynchronizers } from './crosshairsSynchronizers';
import { annotateTextUtils } from './annotateTextUtils';
import { textMarkerUtils } from './textMarkerUtils';
let activeTool = 'wwwc'; let activeTool = 'wwwc';
let defaultTool = 'wwwc'; let defaultTool = 'wwwc';
@ -167,16 +169,34 @@ export const toolManager = {
...shadowConfig ...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 // Set the configuration values for the text annotation (Arrow) tool
/*const annotateConfig = { const annotateConfig = {
getTextCallback: getAnnotationTextCallback, getTextCallback: annotateTextUtils.getTextCallback,
changeTextCallback: changeAnnotationTextCallback, changeTextCallback: annotateTextUtils.changeTextCallback,
drawHandles: false, drawHandles: false,
arrowFirst: true arrowFirst: true
}; };
arrowAnnotate.setConfiguration(annotateConfig);*/ arrowAnnotate.setConfiguration(annotateConfig);
const zoomConfig = { const zoomConfig = {
minScale: 0.05, minScale: 0.05,