Allowing W/L Presets customization

This commit is contained in:
Bruno Alves de Faria 2017-05-01 14:04:26 -03:00 committed by Erik Ziegler
parent 4d9300f6c4
commit e6b5a21ae2
14 changed files with 303 additions and 173 deletions

View File

@ -31,6 +31,7 @@ import './templates/form.html';
import './templates/input.html';
import './templates/link.html';
import './templates/select.html';
import './templates/tr.html';
// wrappers
import './wrappers/label.html';

View File

@ -141,19 +141,23 @@ OHIF.mixins.schemaData = new OHIF.Mixin({
component.parseData = value => {
// Get the current schema data using component's key
const { currentSchema } = getCurrentSchemaDefs(component.parent, instance.data.pathKey);
const { dataType } = instance.data;
// Stop here if there's no schema data for current key
if (!currentSchema) {
// Stop here if there's no schema data for current key or no dataType defined
if (!currentSchema && !dataType) {
return value;
}
// Check if the schema is a Number
if (currentSchema.type === Number) {
// Get the schema type
const schemaType = currentSchema && currentSchema.type;
// Check if the type is Number
if (schemaType === Number || dataType === 'Number') {
return parseFloat(value);
}
// Check if the schema is a Boolean
if (currentSchema.type === Boolean) {
// Check if the type is Boolean
if (schemaType === Boolean || dataType === 'Boolean') {
return !!value;
}

View File

@ -0,0 +1,9 @@
<template name="baseTr">
<tr
id="{{this.id}}"
class="{{this.class}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
</tr>
</template>

View File

@ -1,7 +1,7 @@
<template name="group">
{{#baseComponent (extend this
class=(concat 'component-group ' this.class)
base='baseDiv'
base=(choose this.base 'baseDiv')
mixins=(concat 'group ' this.mixins)
)}}
{{>UI.contentBlock}}

View File

@ -15,3 +15,7 @@ Template.registerHelper('sum', (...values) => {
Template.registerHelper('isValidNumber', value => {
return typeof value === 'number' && !isNaN(value);
});
Template.registerHelper('filterNaN', value => {
return isNaN(value) ? '' : value;
});

View File

@ -11,7 +11,7 @@ Template.registerHelper('concat', (...args) => {
const values = _.initial(args, 1);
let result = '';
_.each(values, value => {
result += value || '';
result += typeof value !== 'undefined' ? value : '';
});
return result;
});

View File

@ -42,7 +42,7 @@ HP.setEngine = protocolEngine => {
Meteor.startup(() => {
HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.create(null), (element, optionValue) => {
if (optionValue in OHIF.viewer.wlPresets) {
if (_.findWhere(OHIF.viewer.wlPresets, { id: optionValue })) {
OHIF.viewerbase.wlPresets.applyWLPreset(optionValue, element);
}
});
@ -113,7 +113,7 @@ HP.ProtocolEngine = class ProtocolEngine {
* with the given study. The best protocol are orded by score and returned in an array
* @param {Object} study StudyMetadata instance object
* @return {Array} Array of match objects or an empty array if no match was found
* Each match object has the score of the matching and the matched
* Each match object has the score of the matching and the matched
* protocol
*/
findMatchByStudy(study) {
@ -122,7 +122,7 @@ HP.ProtocolEngine = class ProtocolEngine {
const matched = [];
const studyInstance = study.getFirstInstance();
// Set custom attribute for study metadata
// Set custom attribute for study metadata
const numberOfAvailablePriors = this.getNumberOfAvailablePriors(study.getObjectID());
HP.ProtocolStore.getProtocol().forEach(protocol => {
@ -318,7 +318,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// Set the custom attribute abstractPriorValue for the study metadata
studyMetadata.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue);
// Also add custom attribute
// Also add custom attribute
const firstInstance = studyMetadata.getFirstInstance();
if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue);
@ -329,7 +329,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// Update the viewport to refresh layout manager with new study
this.updateViewports(viewportIndex);
}, error => {
}, error => {
OHIF.log.warn(error);
throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`);
});
@ -650,7 +650,7 @@ HP.ProtocolEngine = class ProtocolEngine {
}
/**
* Changes the current stage to a new stage index in the display set sequence.
* Changes the current stage to a new stage index in the display set sequence.
* It checks if the next stage exists.
*
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
@ -673,7 +673,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// Set stage Session variable for reactivity
Session.set('HangingProtocolStage', this.stage);
// Since stage has changed, we need to update the viewports
// Since stage has changed, we need to update the viewports
// and redo matchings
this.updateViewports();
@ -682,7 +682,7 @@ HP.ProtocolEngine = class ProtocolEngine {
}
/**
* Retrieves the number of Stages in the current Protocol or
* Retrieves the number of Stages in the current Protocol or
* undefined if no protocol or stages are set
*/
getNumProtocolStages() {
@ -710,7 +710,7 @@ HP.ProtocolEngine = class ProtocolEngine {
*/
previousProtocolStage() {
OHIF.log.info('ProtocolEngine::previousProtocolStage');
if (!this.setCurrentProtocolStage(-1)) {
// Just for logging purpose
OHIF.log.info('ProtocolEngine::previousProtocolStage failed');

View File

@ -6,13 +6,13 @@ import { OHIF } from 'meteor/ohif:core';
import { HotkeysContext } from 'meteor/ohif:hotkeys/client/classes/HotkeysContext';
export class HotkeysManager {
constructor(retrieveFunction, storeFunction) {
constructor() {
this.contexts = {};
this.defaults = {};
this.currentContextName = null;
this.enabled = new ReactiveVar(true);
this.retrieveFunction = retrieveFunction;
this.storeFunction = storeFunction;
this.retrieveFunction = null;
this.storeFunction = null;
this.changeObserver = new Tracker.Dependency();
Tracker.autorun(() => {
@ -21,6 +21,14 @@ export class HotkeysManager {
});
}
setRetrieveFunction(retrieveFunction) {
this.retrieveFunction = retrieveFunction;
}
setStoreFunction(storeFunction) {
this.storeFunction = storeFunction;
}
store(contextName, definitions) {
const storageKey = `hotkeysDefinitions.${contextName}`;
return new Promise((resolve, reject) => {
@ -73,7 +81,11 @@ export class HotkeysManager {
const context = this.getContext(contextName);
if (!context) return;
this.retrieve(contextName).then(definitions => {
if (!definitions) return reject();
if (!definitions) {
this.changeObserver.changed();
return reject();
}
context.extend(definitions);
this.changeObserver.changed();
resolve(definitions);

View File

@ -1,6 +1,6 @@
<template name="hotkeysDialog">
{{#dialogSimple (extend this dialogClass='dialog-hotkeys' title='Keyboard Shortcuts')}}
{{>hotkeysForm this}}
{{! >hotkeysForm this}}
<hr>
{{>windowLevelPresetsForm this}}
{{/dialogSimple}}

View File

@ -19,9 +19,14 @@ OHIF.user.getData = key => {
// Get the user persistent data
const data = profile && profile.persistent;
if (data) {
return data[key];
}
let result = data;
const keys = key.split('.');
keys.forEach(key => {
if (typeof result !== 'object') return;
result = result[key];
});
return result;
};
// Store the persistent data by giving a key and a value to store

View File

@ -1,6 +1,25 @@
<template name="windowLevelPresetsForm">
{{#form (extend this class='form-themed' api=instance.api)}}
{{>inputText label='W/L Preset 1' key='WLPreset1'}}
<table>
<thead>
<tr>
<th>Preset</th>
<th>Name</th>
<th>Window Level (WL)</th>
<th>Window Width (WW)</th>
</tr>
</thead>
<tbody>
{{#each presetInputInformation in getPresetsInputInformationList}}
{{#group base='baseTr' key=(concat @index)}}
<td>{{sum @index 1}}</td>
<td>{{>inputText key='id' value=presetInputInformation.id}}</td>
<td>{{>inputText key='wc' value=(filterNaN presetInputInformation.wc) dataType='Number'}}</td>
<td>{{>inputText key='ww' value=(filterNaN presetInputInformation.ww) dataType='Number'}}</td>
{{/group}}
{{/each}}
</tbody>
</table>
<hr>
<div class="clearfix">
{{#button class='btn btn-primary pull-right' action='save'}}Save{{/button}}

View File

@ -1,14 +1,16 @@
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
Template.windowLevelPresetsForm.onCreated(() => {
const instance = Template.instance();
const { wlPresets } = OHIF.viewerbase;
instance.api = {
save() {
const form = instance.$('form').first().data('component');
const definitions = form.value();
// TODO: return save method with promise
wlPresets.store(definitions);
},
resetDefaults() {
@ -17,9 +19,14 @@ Template.windowLevelPresetsForm.onCreated(() => {
message: 'Are you sure you want to reset all the window level presets to their defaults?'
};
return OHIF.ui.showDialog('dialogConfirm', dialogOptions).then(() => {
// TODO: return reset method with promise
});
return OHIF.ui.showDialog('dialogConfirm', dialogOptions).then(() => wlPresets.resetDefaults());
}
};
});
Template.windowLevelPresetsForm.helpers({
getPresetsInputInformationList() {
OHIF.viewerbase.wlPresets.changeObserver.depend();
return _.toArray(OHIF.viewer.wlPresets);
}
});

View File

@ -1,141 +1,213 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
import { viewportUtils } from './viewportUtils';
const WL_PRESET_CUSTOM = 'Custom';
const WL_PRESET_DEFAULT = 'Default';
const WL_STORAGE_KEY = `WindowLevelPresetsDefinitions`;
OHIF.viewer.defaultWLPresets = {
0: {
id: 'SoftTissue',
wc: 40,
ww: 400
},
1: {
id: 'Lung',
wc: -600,
ww: 1500
},
2: {
id: 'Liver',
wc: 90,
ww: 150
},
3: {
id: 'Bone',
wc: 480,
ww: 2500
},
4: {
id: 'Brain',
wc: 40,
ww: 80
},
5: {},
6: {},
7: {},
8: {},
9: {}
};
class WindowLevelPresetsManager {
constructor() {
this.defaults = {};
this.retrieveFunction = null;
this.storeFunction = null;
this.changeObserver = new Tracker.Dependency();
}
updateElementWLPresetData(element) {
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const enabledElement = cornerstone.getEnabledElement(element);
const { viewport, image } = enabledElement;
const { windowCenter, windowWidth } = viewport.voi;
let preset, presetName;
if (windowWidth === image.windowWidth && windowCenter === image.windowCenter) {
presetName = WL_PRESET_DEFAULT;
} else {
const WLPresets = OHIF.viewer.wlPresets;
for (let index in WLPresets) {
const currentPreset = WLPresets[index];
if (windowCenter === currentPreset.wc && windowWidth === currentPreset.ww) {
preset = currentPreset;
presetName = preset.id;
break;
}
}
}
wlPresetData.name = presetName || WL_PRESET_CUSTOM;
wlPresetData.ww = windowWidth;
wlPresetData.wc = windowCenter;
if (wlPresetData.name === WL_PRESET_CUSTOM) {
const custom = wlPresetData.custom || (wlPresetData.custom = Object.create(null));
custom.ww = windowWidth;
custom.wc = windowCenter;
}
}
/**
* Set specified W/L preset on given element on fallback to default W/L preset if the specified preset is not valid.
* @param {String} presetName The desired W/L preset to be applied
* @param {DOMElement} element An enabled viewport DOM Element.
*/
applyWLPreset(presetName, element) {
const wlPresets = OHIF.viewer.wlPresets;
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const viewport = cornerstone.getViewport(element);
const preset = wlPresets[presetName] || _.findWhere(wlPresets, { id: presetName });
if (presetName === WL_PRESET_CUSTOM && wlPresetData.custom) {
viewport.voi.windowWidth = wlPresetData.custom.ww;
viewport.voi.windowCenter = wlPresetData.custom.wc;
} else if (preset && !_.isEmpty(preset) && preset.id) {
presetName = preset.id;
viewport.voi.windowWidth = preset.ww;
viewport.voi.windowCenter = preset.wc;
} else {
const enabledElement = cornerstone.getEnabledElement(element);
viewport.voi.windowWidth = enabledElement.image.windowWidth;
viewport.voi.windowCenter = enabledElement.image.windowCenter;
presetName = WL_PRESET_DEFAULT;
}
wlPresetData.name = presetName;
wlPresetData.ww = viewport.voi.windowWidth;
wlPresetData.wc = viewport.voi.windowCenter;
// Update the viewport
cornerstone.setViewport(element, viewport);
OHIF.log.info('WLPresets::Applying WL Preset: ' + presetName);
// Notify other components about W/L Preset changes
Session.set('OHIFWlPresetApplied', presetName);
}
store(wlPresets) {
return new Promise((resolve, reject) => {
if (this.storeFunction) {
this.storeFunction(wlPresets).then(resolve).catch(reject);
} else if (Meteor.userId()) {
OHIF.user.setData(WL_STORAGE_KEY, wlPresets).then(resolve).catch(reject);
} else {
Session.setPersistent(WL_STORAGE_KEY, wlPresets);
resolve();
}
}).then(() => this.setOHIFWLPresets(wlPresets));
}
retrieve() {
return new Promise((resolve, reject) => {
if (this.retrieveFunction) {
this.retrieveFunction().then(resolve).catch(reject);
} else if (Meteor.userId()) {
try {
resolve(OHIF.user.getData(WL_STORAGE_KEY));
} catch(error) {
reject(error);
}
} else {
resolve(Session.get(WL_STORAGE_KEY));
}
});
}
load() {
return new Promise((resolve, reject) => {
this.retrieve().then(wlPresets => {
if (wlPresets) {
this.setOHIFWLPresets(wlPresets);
} else {
this.loadDefauls();
}
}).catch(this.loadDefauls);
});
}
applyWLPresetToActiveElement(presetName) {
const element = viewportUtils.getActiveViewportElement();
if (!element) {
return;
}
this.applyWLPreset(presetName, element);
}
/**
* Overrides OHIF's wlPresets
* @param {Object} wlPresets Object with wlPresets mapping
*/
setOHIFWLPresets(wlPresets) {
const hasOwn = Object.prototype.hasOwnProperty;
const presetMap = Object.create(null); // Objects without prototype have much faster lookup times
for (let index in wlPresets) {
if (hasOwn.call(wlPresets, index)) {
presetMap[index] = wlPresets[index];
}
}
OHIF.viewer.wlPresets = presetMap;
this.changeObserver.changed();
}
loadDefauls() {
this.setOHIFWLPresets(OHIF.viewer.defaultWLPresets);
}
resetDefaults() {
return this.store(OHIF.viewer.defaultWLPresets);
}
}
// TODO: add this to a namespace definition
Meteor.startup(function() {
OHIF.viewer.defaultWLPresets = {
SoftTissue: {
wc: 40,
ww: 400
},
Lung: {
wc: -600,
ww: 1500
},
Liver: {
wc: 90,
ww: 150
},
Bone: {
wc: 480,
ww: 2500
},
Brain: {
wc: 40,
ww: 80
}
};
setOHIFWLPresets(OHIF.viewer.defaultWLPresets);
});
function updateElementWLPresetData(element) {
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const enabledElement = cornerstone.getEnabledElement(element);
const { viewport, image } = enabledElement;
const { windowCenter, windowWidth } = viewport.voi;
let presetName;
if (windowWidth === image.windowWidth && windowCenter === image.windowCenter) {
presetName = WL_PRESET_DEFAULT;
} else {
const WLPresets = OHIF.viewer.wlPresets;
for (let name in WLPresets) {
const preset = WLPresets[name];
if (windowCenter === preset.wc && windowWidth === preset.ww) {
presetName = name;
break;
}
}
}
wlPresetData.name = presetName || WL_PRESET_CUSTOM;
wlPresetData.ww = windowWidth;
wlPresetData.wc = windowCenter;
if (wlPresetData.name === WL_PRESET_CUSTOM) {
const custom = wlPresetData.custom || (wlPresetData.custom = Object.create(null));
custom.ww = windowWidth;
custom.wc = windowCenter;
}
}
/**
* Set specified W/L preset on given element on fallback to default W/L preset if the specified preset is not valid.
* @param {String} presetName The desired W/L preset to be applied
* @param {DOMElement} element An enabled viewport DOM Element.
*/
function applyWLPreset(presetName, element) {
const wlPresets = OHIF.viewer.wlPresets;
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const viewport = cornerstone.getViewport(element);
if (presetName === WL_PRESET_CUSTOM && wlPresetData.custom) {
viewport.voi.windowWidth = wlPresetData.custom.ww;
viewport.voi.windowCenter = wlPresetData.custom.wc;
} else if (presetName in wlPresets) {
const preset = wlPresets[presetName];
viewport.voi.windowWidth = preset.ww;
viewport.voi.windowCenter = preset.wc;
} else {
const enabledElement = cornerstone.getEnabledElement(element);
viewport.voi.windowWidth = enabledElement.image.windowWidth;
viewport.voi.windowCenter = enabledElement.image.windowCenter;
presetName = WL_PRESET_DEFAULT;
}
wlPresetData.name = presetName;
wlPresetData.ww = viewport.voi.windowWidth;
wlPresetData.wc = viewport.voi.windowCenter;
// Update the viewport
cornerstone.setViewport(element, viewport);
OHIF.log.info('WLPresets::Applying WL Preset: ' + presetName);
// Notify other components about W/L Preset changes
Session.set('OHIFWlPresetApplied', presetName);
}
function applyWLPresetToActiveElement(presetName) {
const element = viewportUtils.getActiveViewportElement();
if (!element) {
return;
}
applyWLPreset(presetName, element);
}
/**
* Overrides OHIF's wlPresets
* @param {Object} wlPresets Object with wlPresets mapping
*/
function setOHIFWLPresets(wlPresets) {
const hasOwn = Object.prototype.hasOwnProperty;
const presetMap = Object.create(null); // Objects without prototype have much faster lookup times
for (let name in wlPresets) {
if (hasOwn.call(wlPresets, name)) {
presetMap[name] = wlPresets[name];
}
}
OHIF.viewer.wlPresets = presetMap;
}
/**
* Export functions inside WLPresets namespace.
*/
const WLPresets = new WindowLevelPresetsManager();
const WLPresets = {
applyWLPreset,
applyWLPresetToActiveElement,
setOHIFWLPresets,
updateElementWLPresetData
};
Meteor.startup(() => {
WLPresets.load();
});
export { WLPresets };

View File

@ -65,11 +65,16 @@ Meteor.startup(function() {
toggleCineDialog: '',
// Preset hotkeys
WLPresetSoftTissue: '1',
WLPresetLung: '2',
WLPresetLiver: '3',
WLPresetBone: '4',
WLPresetBrain: '5'
WLPreset0: '1',
WLPreset1: '2',
WLPreset2: '3',
WLPreset3: '4',
WLPreset4: '5',
WLPreset5: '6',
WLPreset6: '7',
WLPreset7: '8',
WLPreset8: '9',
WLPreset9: '0'
};
// For now
@ -140,23 +145,15 @@ Meteor.startup(function() {
clearTools: 'Clear Tools'
});
// Functions to register the preset switching commands
const registerWLPresetCommands = map => _.each(map, (commandName, presetName) => {
OHIF.commands.register(contextName, presetName, {
name: commandName,
action: WLPresets.applyWLPresetToActiveElement,
params: presetName.replace('WLPreset', '')
});
});
// Register the preset switching commands
registerWLPresetCommands({
WLPresetSoftTissue: 'W/L Preset: Soft Tissue',
WLPresetLung: 'W/L Preset: Lung',
WLPresetLiver: 'W/L Preset: Liver',
WLPresetBone: 'W/L Preset: Bone',
WLPresetBrain: 'W/L Preset: Brain'
});
const applyPreset = presetName => WLPresets.applyWLPresetToActiveElement(presetName);
for (let i = 0; i < 10; i++) {
OHIF.commands.register(contextName, `WLPreset${i}`, {
name: `W/L Preset ${i + 1}`,
action: applyPreset,
params: i
});
}
// Register viewport navigation commands
OHIF.commands.set(contextName, {