ohif-viewer/Packages/viewerbase/lib/windowManager.js

657 lines
21 KiB
JavaScript

/**
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
*/
function getMammoHangingProtocolObject() {
var protocol = [{
stage: 1,
rows: 2,
columns: 4,
viewports: [{
seriesDescription: 'RCC',
study: 'prior'
}, {
seriesDescription: 'LCC',
study: 'prior'
}, {
seriesDescription: 'RMLO',
study: 'prior'
}, {
seriesDescription: 'LMLO',
study: 'prior'
}, {
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'current'
}, {
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'current'
}
]
}, {
stage: 2,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'current'
}
]
}, {
stage: 3,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'current'
}
]
}, {
stage: 4,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RCC',
study: 'current'
}, {
seriesDescription: 'RCC',
study: 'prior'
}
]
}, {
stage: 5,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'LCC',
study: 'current'
}, {
seriesDescription: 'LCC',
study: 'prior'
}
]
}, {
stage: 6,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'LMLO',
study: 'current'
}, {
seriesDescription: 'LMLO',
study: 'prior'
}
]
}, {
stage: 7,
rows: 1,
columns: 2,
viewports: [{
seriesDescription: 'RMLO',
study: 'current'
}, {
seriesDescription: 'RMLO',
study: 'prior'
}
]
}, {
stage: 8,
rows: 2,
columns: 4,
viewports: [{
seriesDescription: 'RCC',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LCC',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RMLO',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LMLO',
study: 'prior',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RCC',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LCC',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'RMLO',
study: 'current',
options: {
includeCADMarkers: true
}
}, {
seriesDescription: 'LMLO',
study: 'current',
options: {
includeCADMarkers: true
}
}
]
}
];
return protocol;
}
function findSeriesByDescription(seriesDescription, study) {
var seriesInstanceUid;
study.seriesList.forEach(function(series) {
if (!series.seriesDescription) {
return;
}
var currentSeriesDescription = series.seriesDescription.replace(' ', '');
if (currentSeriesDescription === seriesDescription) {
seriesInstanceUid = series.seriesInstanceUid;
return false;
}
});
return seriesInstanceUid;
}
/**
* (Work in progress) Uses the information from a DICOM Hanging Protocol
* to identify and display studies and series in the image viewer
*
* @param hangingProtocol
* @param inputData
* @returns {Array} Array of viewport data to be displayed
*/
function applyHangingProtocol(hangingProtocol, inputData) {
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
var studies = ViewerStudies;
var currentProtocolData = hangingProtocol[presentationGroup - 1];
var viewportData = {
viewports: [],
viewportRows: currentProtocolData.rows,
viewportColumns: currentProtocolData.columns
};
var currentStudy = ViewerStudies.find({}, {$sort: {studyDate: 1}}).fetch()[0];
var otherStudies = WorklistStudies.find({
patientId: currentStudy.patientId,
studyInstanceUid: {
$ne: currentStudy.studyInstanceUid
}
}, {$sort: {
studyDate: 1
}}).fetch();
var priorStudy = otherStudies[0];
currentProtocolData.viewports.forEach(function(viewport, index) {
if (viewport.study === 'current') {
study = currentStudy;
} else if (viewport.study === 'prior') {
study = currentStudy;
/*ViewerStudies.find({
studyInstanceUid: {
$ne: currentStudy.studyInstanceUid
}
}, {$sort: {studyDate: 1}}).fetch()[0];*/
}
seriesInstanceUid = findSeriesByDescription(viewport.seriesDescription, study);
viewportData.viewports[index] = {
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: study.studyInstanceUid,
currentImageIdIndex: 0,
options: viewport.options
};
});
return viewportData;
}
/**
* This is a temporary function which will return a hardcoded hanging protocol as a JavaScript object
* The purpose of this is to act as a stub until we are actually parsing DICOM Hanging Protocol files,
* since Orthanc doesn't seem to support them yet.
*
* Soon we will be using instanceDataToJsObject to produce this object from the HangingProtocol WADO
* instance
*
*/
function getMammoHangingProtocol() {
var tagValues = {
HangingProtocolStorage: '1.2.840.10008.5.1.4.38.1'
};
var protocol = {
sopClassUid: tagValues.HangingProtocolStorage,
sopInstanceUid: '1.2.840.113986.2.664566.21121125.85669.911', // A random Uid
hangingProtocolName: 'MammoCadProtocol',
hangingProtocolDescription: 'Mammography screening protocol',
hangingProtocolLevel: 'SITE',
hangingProtocolCreator: 'Erik',
hangingProtocolCreationDateTime: '20020101104200',
hangingProtocolDefinitionSequence: [{
modality: 'MG',
laterality: '',
procedureCodeSequence: {
codeValue: 98765,
codingSchemeDesignator: '99Local',
codingSchemeVersion: 1.5,
codeMeaning: 'Mammogram'
},
anatomicRegionSequence: {
codeValue: 'T-D1100',
codingSchemeDesignator: 'SNM3',
codeMeaning: 'BREAST'
},
reasonForRequestedProcedureCodeSequence: {
codeValue: 'I67.1',
codingSchemeDesignator: 'I10',
codeMeaning: 'Calcification'
}
}],
hangingProtocolUserIdentificationCodeSequence: [],
hangingProtocolUserGroupName: 'ABC Hospital',
numberOfPriorsReferenced: 1,
imageSetsSequence: [{
imageSetSelectorSequence: [{
ImageSetSelectorUsageFlag: 'NO_MATCH',
SelectorAttribute: '00180015',
SelectorValueNumber: '1',
SelectorAttributeVR: 'CS',
SelectorCSValue: 'BREAST'
}],
timeBasedImageSetsSequence: [{
ImageSetNumber: 1,
ImageSetSelectorCategory: 'RELATIVE_TIME',
RelativeTime: '0\0',
RelativeTimeUnits: 'MINUTES',
ImageSetLabel: 'Current MG Breast'
},
{
ImageSetNumber: 2,
ImageSetSelectorCategory: 'ABSTRACT_PRIOR',
AbstractPriorValue: '1\\1',
ImageSetLabel: 'Prior MG Breast'
}]
}],
// Skip Number of Screens for now,
// Skip Nominal Screen Definition Sequence for now,
// http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.23.3.html
DisplaySetsSequence: [
{
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 1,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Right side image (L MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 1,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'L CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
},
{
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 2,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}],
ReformattingOperationType: '',
ReformattingThickness: '',
ReformattingInterval: '',
ReformattingOperationInitialViewDirection: '',
SortingOperationsSequence: [],
DisplaySetPatientOrientation: '',
VOIType: ''
}, {
// Right side image (L MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 2,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'L MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Testing purposes
// Left side image (R MLO Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 3,
DisplaySetPresentationGroupDescription: 'Current Mediolateral only',
ImageBoxesSequence: [{
DisplayEnvironmentSpatialPosition: '0\0.2\0.16667\0',
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R MLO',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}, {
// Right side image (R CC Current)
ImageSetNumber: 1,
DisplaySetNumber: 1,
DisplaySetPresentationGroup: 3,
ImageBoxesSequence: [{
ImageBoxNumber: 1,
ImageBoxLayoutType: 'STACK'
}],
FilterOperationsSequence: [{
SelectorAttributeVR: 'CS',
SelectorCSValue: 'R CC',
FilterByCategory: 'SERIES_DESCRIPTION',
FilterByOperator: 'MEMBER_OF'
}]
}],
PartialDataDisplayHandling: 'MAINTAIN_LAYOUT',
SynchronizedScrollingSequence: [],
NavigationIndicatorSequence: []
};
return protocol;
}
function findSetsByPresentationGroup(DisplaySetsSequence, DisplaySetPresentationGroup) {
var presentationGroupSets = [];
DisplaySetsSequence.forEach(function(displaySet) {
if (displaySet.DisplaySetPresentationGroup === DisplaySetPresentationGroup) {
presentationGroupSets.push(displaySet);
}
});
return presentationGroupSets;
}
function findStudy(displaySet, studies) {
// Placeholder for now
return studies.find().fetch()[0].studyInstanceUid;
}
/**
* Uses the FilterOperationsSequence information to find
* the relevant series for display in this display set
*
* @param displaySet
* @param study
* @returns {String} The instance Uid of the series to be displayed
*/
function findSeries(displaySet, study) {
var categoryDictionary = {
SERIES_DESCRIPTION: 'seriesDescription'
};
// TODO=Put these inside a loop? We need to support multiple filter operations,
// not just one
var filterBy = displaySet.FilterOperationsSequence[0].FilterByCategory;
var selectorValue = displaySet.FilterOperationsSequence[0].SelectorCSValue;
var filterType = displaySet.FilterOperationsSequence[0].FilterByOperator;
var category = categoryDictionary[filterBy];
var seriesInstanceUid;
study.seriesList.forEach(function(series) {
// TODO = Add other FilterBy operators
if (filterType === 'MEMBER_OF') {
if (series[category] === selectorValue) {
seriesInstanceUid = series.seriesInstanceUid;
return false;
}
}
});
return seriesInstanceUid;
}
/**
* (Work in progress) Uses the information from a DICOM Hanging Protocol
* to identify and display studies and series in the image viewer
*
* @param hangingProtocol
* @param inputData
* @returns {Array} Array of viewport data to be displayed
*/
function applyDICOMHangingProtocol(hangingProtocol, inputData) {
log.info('applyDICOMHangingProtocol');
var presentationGroup = inputData.DisplaySetPresentationGroup || 1;
var studies = ViewerStudies;
var currentDisplaySets = findSetsByPresentationGroup(hangingProtocol.DisplaySetsSequence, presentationGroup);
var viewportData = {
viewports: []
};
currentDisplaySets.forEach(function(displaySet, index) {
// TODO= Find study information by image set properties and
// image set number of this display set
var ImageSetNumber = displaySet.ImageSetNumber;
studyInstanceUid = findStudy(displaySet, studies);
var study = ViewerStudies.findOne({studyInstanceUid: studyInstanceUid});
seriesInstanceUid = findSeries(displaySet, study);
viewportData.viewports[index] = {
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: studyInstanceUid,
currentImageIdIndex: 0
};
});
return viewportData;
}
/**
* This is an example of a hanging protocol
* It takes in a set of studies, as well as the
* number of rows and columns in the layout.
*
* It returns an array of objects, one for each viewport, detailing
* which series should be loaded in the viewport.
*
* @param inputData
* @returns {Array}
*/
function defaultHangingProtocol(inputData) {
var studies = ViewerStudies.find().fetch();
var viewportRows = inputData.viewportRows;
var viewportColumns = inputData.viewportColumns;
// This is the most basic hanging protocol.
var stacks = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
// Ensure that the series has image data
// (All images have rows)
var anInstance = series.instances[0];
if (!anInstance || !anInstance.rows) {
return;
}
var stack = {
series: series,
study: study
};
stacks.push(stack);
});
});
var viewportData = {
viewports: []
};
var numViewports = viewportRows * viewportColumns;
for (var i=0; i < numViewports; ++i) {
if (i >= stacks.length) {
// We don't have enough stacks to fill the desired number of viewports, so stop here
break;
}
viewportData.viewports[i] = {
seriesInstanceUid: stacks[i].series.seriesInstanceUid,
studyInstanceUid: stacks[i].study.studyInstanceUid,
currentImageIdIndex: 0
};
}
return viewportData;
}
var hangingProtocol;
var presentationGroup = 1;
function getHangingProtocol(inputData) {
// TODO = Update this to use Collection logic
var studies = ViewerStudies.find().fetch();
if (!studies.length) {
log.warn("No studies provided to Hanging Protocol");
return;
}
// Find the unique modalities in this study
var modalities = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
if (modalities.indexOf(series.modality) < 0) {
modalities.push(series.modality);
}
});
});
if (modalities.indexOf('MG') > -1) {
var hp = getMammoHangingProtocolObject();
Session.set('WindowManagerPresentationGroup', presentationGroup);
var testData = applyHangingProtocol(hp, inputData);
return testData;
// Commenting this out for now so we can hardcode a MG protocol
/*var hangingProtocol = getMammoHangingProtocol();
Session.set('WindowManagerPresentationGroup', presentationGroup);
return applyDICOMHangingProtocol(hangingProtocol, inputData);*/
}
Session.set('WindowManagerPresentationGroup', undefined);
return defaultHangingProtocol(inputData);
}
function setHangingProtocol(protocol) {
hangingProtocol = protocol;
}
function previousPresentationGroup() {
presentationGroup--;
presentationGroup = Math.max(1, presentationGroup);
log.info('previousPresentationGroup: ' + presentationGroup);
Session.set('WindowManagerPresentationGroup', presentationGroup);
Session.set('UseHangingProtocol', Random.id());
}
function nextPresentationGroup() {
var numPresentationGroups = getNumPresentationGroups();
presentationGroup++;
presentationGroup = Math.min(numPresentationGroups, presentationGroup);
log.info('nextPresentationGroup: ' + presentationGroup);
Session.set('WindowManagerPresentationGroup', presentationGroup);
Session.set('UseHangingProtocol', Random.id());
}
function getNumPresentationGroups() {
// TODO=Pull this information from the largest
// value of DisplaySetPresentationGroup in the DisplaySetsSequence
// after the DICOM-HP is parsed
return 8;
}
function getCurrentPresentationGroup() {
return presentationGroup;
}
function setCurrentPresentationGroup(groupNumber) {
// Unused for now
presentationGroup = groupNumber;
}
WindowManager = {
setHangingProtocol: setHangingProtocol,
getHangingProtocol: getHangingProtocol,
getNumPresentationGroups: getNumPresentationGroups,
setCurrentPresentationGroup: setCurrentPresentationGroup,
getCurrentPresentationGroup: getCurrentPresentationGroup,
nextPresentationGroup: nextPresentationGroup,
previousPresentationGroup: previousPresentationGroup
};
WindowManager.setHangingProtocol(defaultHangingProtocol);