feat(measurement-table) Display support for measurements from DICOM Structured Reports (#225)

This commit is contained in:
Gustavo André Lelis authored and Erik Ziegler committed 2018-07-20 12:40:09 +02:00
1 parent e9832f62f7
commit d94f0eec16
64 files changed
+1797 -78

No files matched your search

@@ -1,4 +1,5 @@
import './association';
import './caseProgress';
import './measurementTable';
import './measurementLightTable';
import './measureFlow';
@@ -0,0 +1,22 @@
// Measurement Table Components imports
import './measurementEditDescription/measurementEditDescription.html';
import './measurementEditDescription/measurementEditDescription.js';
import './measurementLightTable.html';
import './measurementLightTable.styl';
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.html';
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl';
import './measurementLightTableRow/measurementLightTableRow.html';
import './measurementLightTableRow/measurementLightTableRow.styl';
import './measurementLightTableRow/measurementLightTableRow.js';
import './measurementLightTableView/measurementLightTableView.html';
import './measurementLightTableView/measurementLightTableView.styl';
import './measurementLightTableView/measurementLightTableView.js';
import './measurementRelabel/measurementRelabel.html';
import './measurementRelabel/measurementRelabel.styl';
import './measurementRelabel/measurementRelabel.js';
@@ -0,0 +1,13 @@
<template name="measurementEditDescription">
{{#dialogForm (extend this
dialogClass='modal-sm'
api=instance.api
)}}
{{>inputText
labelClass='form-group'
key='description'
hideSearch=true
options=(clone placeholder='Add a description')
}}
{{/dialogForm}}
</template>
@@ -0,0 +1,26 @@
import { Template } from 'meteor/templating';
import { OHIF } from 'meteor/ohif:core';
Template.measurementEditDescription.onRendered(() => {
const instance = Template.instance();
const form = instance.$('form').data('component');
const measurementData = instance.data.measurementData;
const collection = OHIF.viewer.measurementApi.tools[measurementData.toolType];
const currentMeasurement = collection.findOne({ _id: measurementData._id });
if (currentMeasurement.description) {
form.value({
description: currentMeasurement.description
});
}
// Update the description after confirming the dialog data
instance.data.promise.then(formData => {
collection.update({
_id: measurementData._id,
}, {
$set: { description: formData.description }
});
});
});
@@ -0,0 +1,17 @@
<template name="measurementLightTable">
<div id="measurementLightTableContainer" class="flex-v">
<div class="measurementLightTableHeaderContainer">
<div class="measurementLightTableHeader">
<div class="studyDateLabel">
Study date:
</div>
<div class="studyDate">
{{formatDA this.timepointApi.all.[0].latestDate "DD-MMM-YY"}}
</div>
</div>
</div>
{{#scrollArea class='measurementLightTableView flex-grow fit' scrollStep=63}}
{{>measurementLightTableView (clone this)}}
{{/scrollArea}}
</div>
</template>
@@ -0,0 +1,40 @@
@require '{ohif:design}/app'
#measurementLightTableContainer
theme('background-color', '$primaryBackgroundColor')
height: 100%
width: 100%
.measurementLightTableHeaderContainer
display: flex
padding: 0 2px 0 44px
position: relative
.measurementLightTableHeader
flex: 1
justify-content: space-around
padding-top: 2px
position: relative
margin: 0.4em 0
.studyDateLabel, .studyDate
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
font-weight: 400
padding-left: 12px
text-align: left
.studyDateLabel
theme('color', '$textSecondaryColor')
font-size: 12px
line-height: 12px
.studyDate
theme('color', '$textPrimaryColor')
font-size: 14px
line-height: 20px
padding-bottom: 6px
.measurementLightTableLayoutChanger
margin: 0 auto
padding: 20px
text-align: center
@@ -0,0 +1,6 @@
<template name="measurementLightTableHeaderRow">
<div class="measurementLightTableHeaderRow">
<div class="type">{{toolGroup.name}}</div>
<div class="numberOfMeasurements">{{measurementRows.length}}</div>
</div>
</template>
@@ -0,0 +1,36 @@
@import "{ohif:design}/app"
$headerRowHeight = 63px
.measurementLightTableHeaderRow
theme('background-color', '$uiGrayDarker')
theme('color', '$textSecondaryColor')
display: flex
theme('fill', '$textSecondaryColor')
height: $headerRowHeight
line-height: $headerRowHeight
margin-top: 2px
width: 100%
div
align-items: stretch
flex: 1
justify-content: space-around
text-align: center
.type
theme('color', '$textSecondaryColor')
font-size: 22px
font-weight: 300
line-height: $headerRowHeight
padding: 0 10px
text-align: left
.numberOfMeasurements
theme('color', '$uiSkyBlue')
float: right
font-weight: 300
font-size: 40px
max-width: 54px
height: $headerRowHeight
line-height: 66px
@@ -0,0 +1,55 @@
<template name="measurementLightTableRow">
<div class="measurementLightTableRow" data-measurementid="{{_id}}">
<div class='measurementRowSidebar'>
<div class="measurementNumber">
{{rowItem.measurementNumber}}
</div>
{{#if rowItem.responseStatus}}
<div class="response-status-icon">
{{rowItem.responseStatus}}
</div>
{{/if}}
</div>
<div class="measurementRowContent">
<div class="measurementDetails">
<label class="location">
{{#if rowItem.location}}
{{rowItem.location}}
{{else}}
(No Location)
{{/if}}
{{#if rowItem.description}}
({{rowItem.description}})
{{/if}}
</label>
<div class="value">
{{#if displayData}}
{{{displayData}}}
{{else}}
...
{{/if}}
</div>
</div>
<div class="rowOptions">
<div class="rowAction js-edit-label pull-left m-r-2" tabindex="1">
<svg class="edit-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
</svg>
Relabel
</div>
<div class="rowAction js-edit-description pull-left m-r-2" tabindex="1">
<svg class="edit-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
</svg>
Description
</div>
<div class="rowAction js-delete pull-left" tabindex="1">
<svg class="close-icon">
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
</svg>
Delete
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,103 @@
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
const getPosition = event => {
return {
x: event.clientX,
y: event.clientY
};
};
Template.measurementLightTableRow.helpers({
displayData() {
const instance = Template.instance();
const { rowItem } = instance.data;
const data = rowItem.entries[0];
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const measurementTools = config.measurementTools;
const toolGroup = measurementTools.find( toolGroup => toolGroup.id === rowItem.measurementTypeId);
const tool = toolGroup.childTools.find( childTool => childTool.id === data.toolType );
if (!tool) {
return 'No measurement value';
}
const { displayFunction } = tool.options.measurementTable;
return displayFunction(data);
}
});
Template.measurementLightTableRow.events({
'click .measurementLightTableRow'(event, instance) {
const $row = instance.$('.measurementLightTableRow');
const rowItem = instance.data.rowItem;
const timepoints = instance.data.timepointApi.all();
$row.closest('.measurementLightTableView').find('.measurementLightTableRow').not($row).removeClass('active');
$row.toggleClass('active');
const childToolKey = $(event.target).attr('data-child');
OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey);
},
'click .js-edit-label'(event, instance) {
event.stopPropagation();
const rowItem = instance.data.rowItem;
const entry = rowItem.entries[0];
// Show the measure flow for measurements
OHIF.measurements.openLocationModal({
measurement: entry,
element: document.body,
measurementApi: instance.data.measurementApi,
position: getPosition(event),
autoClick: true
});
},
'click .js-edit-description'(event, instance) {
const rowItem = instance.data.rowItem;
const entry = rowItem.entries[0];
OHIF.ui.showDialog('measurementEditDescription', {
event,
title: 'Edit Description',
element: event.element,
measurementData: entry
});
},
'click .js-delete'(event, instance) {
event.stopPropagation();
const dialogSettings = {
class: 'themed',
title: 'Delete measurements',
message: 'Are you sure you want to delete the measurement?',
position: getPosition(event)
};
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => {
const measurementTypeId = instance.data.rowItem.measurementTypeId;
const measurement = instance.data.rowItem.entries[0];
const measurementNumber = measurement.measurementNumber;
const { timepointApi, measurementApi } = instance.data;
// Remove all the measurements with the given type and number
measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber });
// Sync the new measurement data with cornerstone tools
const baseline = timepointApi.baseline();
measurementApi.sortMeasurements(baseline.timepointId);
// Repaint the images on all viewports without the removed measurements
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
// Notify that viewer suffered changes
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
});
}
});
@@ -0,0 +1,87 @@
@import "{ohif:design}/app"
.measurementLightTableRow
display: flex
margin-left: -6px
margin-top: 2px
padding-left: 6px
opacity: 0.7
transform(scale(1))
width: calc(100% + 6px)
&:hover
opacity 1
&.active
opacity 1
.measurementRowSidebar
theme('color', '$activeColor')
.rowOptions
height: 35px
visibility: visible
.rowOptions
theme('background-color', '$uiGrayDarker')
height: 0
overflow: hidden
transition(all 0.3s ease)
visibility: hidden
padding-left: 14px
.rowAction
theme('color', '$defaultColor')
cursor: pointer
line-height: 35px
transition(all 0.3s ease)
&:hover, &:active
theme('color', '$textPrimaryColor')
svg
theme('fill', '$textPrimaryColor')
theme('stroke', '$textPrimaryColor')
svg
theme('fill', '$defaultColor')
theme('stroke', '$defaultColor')
transition(all 0.3s ease)
&.edit-icon
width: 14px
height: 13px
&.close-icon
width: 11px
height: 11px
.measurementRowSidebar
theme('background', '$uiGray')
theme('color', '$textSecondaryColor')
cursor: pointer
flex: 1
max-width: 30px
transition(all 0.3s ease)
.measurementNumber
font-size: 14px
font-weight: 400
margin-right: 5px
padding-top: 10px
text-align: center
.measurementRowContent
flex: 1
.measurementDetails
padding: 5px 2px 0 14px
line-height: 30px
font-size: 14px
.location
theme('color', '$textSecondaryColor')
font-weight: 400
margin-left: -2px
.value
theme('color', '$textPrimaryColor')
@@ -0,0 +1,12 @@
<template name="measurementLightTableView">
{{#let config=measurementConfiguration groups=measurementGroups.get}}
{{#each measurementGroup in groups}}
{{>measurementLightTableHeaderRow (clone this
toolGroup=measurementGroup.toolGroup
measurementRows=measurementGroup.measurementRows)}}
{{#each rowItem in measurementGroup.measurementRows}}
{{>measurementLightTableRow (clone this rowItem=rowItem)}}
{{/each}}
{{/each}}
{{/let}}
</template>
@@ -0,0 +1,16 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { OHIF } from 'meteor/ohif:core';
Template.measurementLightTableView.onCreated(() => {
const instance = Template.instance();
const { measurementApi, timepointApi } = instance.data;
instance.data.measurementGroups = new ReactiveVar();
instance.autorun(() => {
measurementApi.changeObserver.depend();
const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi);
instance.data.measurementGroups.set(data);
});
});
@@ -0,0 +1,13 @@
@import "{ohif:design}/app"
.measurementLightTableView
.report-area
theme('background-color', '$uiGrayDarker')
margin-top: 2px
padding: 10px 0
text-align: center
.btn
theme('background-color', '$activeColor')
theme('border', '1px solid $uiBorderColorActive')
color: #000
@@ -0,0 +1,4 @@
<template name="measurementRelabel">
<div class="measurement-relabel {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}" tabindex="-1">
</div>
</template>
@@ -0,0 +1,107 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { $ } from 'meteor/jquery';
import {
segmentedTerminologyList,
segmentedTerminologyCommonList } from '../../../lib/getLabelTerminologyList';
Template.measurementRelabel.onCreated(() => {
const instance = Template.instance();
instance.value = instance.data.currentValue || '';
instance.state = new ReactiveVar('closed');
instance.items = segmentedTerminologyList;
instance.commonItems = segmentedTerminologyCommonList;
});
Template.measurementRelabel.onRendered(() => {
const instance = Template.instance();
const $measurementRelabel = instance.$('.measurement-relabel');
// Make the measure flow bounded by the window borders
$measurementRelabel.bounded();
// Wait template rerender before rendering the selectTree
Tracker.afterFlush(() => {
// Get the click or rendering position
const position = {
left: event.clientX || instance.data.position.x,
top: event.clientY || instance.data.position.y,
};
// Define the data for selectTreeComponent
const data = {
key: 'label',
items: instance.items,
commonItems: instance.commonItems,
hideCommon: instance.data.hideCommon,
label: 'Assign label',
search: true,
searchPlaceholder: 'Search labels',
threeColumns: instance.data.threeColumns,
position
};
// Define in which element the selectTree will be rendered in
const parentElement = $measurementRelabel[0];
// Render the selectTree element
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
// Focus the measure flow to handle closing
$measurementRelabel.focus();
});
});
Template.measurementRelabel.events({
'click, mousedown, mouseup'(event, instance) {
event.stopPropagation();
},
'click .tree-leaf input'(event, instance) {
const $target = $(event.currentTarget);
const $measureFlow = instance.$('.measurement-relabel');
instance.state.set('selected');
instance.value = $target.data('component').value();
instance.data.updateCallback(instance.value);
$measureFlow.trigger('close')
},
'blur .measurement-relabel'(event, instance) {
const $measurementRelabel = $(event.currentTarget);
const element = $measurementRelabel[0];
Meteor.defer(() => {
const focused = $(':focus')[0];
if (element !== focused && !$.contains(element, focused)) {
$measurementRelabel.trigger('close');
}
});
},
'mouseleave .measurement-relabel'(event, instance) {
const $measurementRelabel = $(event.currentTarget);
const canClose = instance.state.get() === 'selected';
if (canClose && !$.contains($measurementRelabel[0], event.toElement)) {
$measurementRelabel.trigger('close');
}
},
'mouseenter .measurement-relabel'(event, instance) {
// Prevent from closing if user go out and in too fast
clearTimeout(instance.closingTimeout);
$(event.currentTarget).off('animationend').removeClass('fadeOut');
},
'close .measurement-relabel'(event, instance) {
// Clear the timeout to prevent executing the close process twice
clearTimeout(instance.closingTimeout);
instance.data.doneCallback();
}
});
@@ -0,0 +1,106 @@
@import "{ohif:design}/app"
.measurement-relabel
outline: none
position: fixed
z-index: 1
&.fadeOut
animateFadeOut()
.select-tree-root>.tree-content
width: 140px
&>.tree-options
position: relative
z-index: 2
.tree-inputs
position: relative
&>.tree-leaf
position: relative
.icon-check
theme('background-color', '$activeColor')
animateZoomIn()
left: -46px
position: absolute
top: 3px
span
background: white
font-weight: normal
height: 46px
line-height: 46px
padding: 0 12px
input, span
display: none
.actions
padding-top: 16px
opacity: 0
&:not(.fadeOut)
animateFadeIn()
animation-delay: 0.3s
&.fadeOut
animateFadeOut()
&.selected>.tree-leaf
span
display: block
&.tree-columns
.select-tree-root
&.navigated .select-tree-common .content
animation-name: none
&.selected .select-tree-common .content
animation-name: selectTreeCommonCloseLeft
.select-tree-common
padding-left: 0
padding-right: 6px
right: auto
left: 0
width: 180px
&>.tree-content
transform-origin(0% 50%)
width: 280px
.select-tree-common .content
right: -180px
&.started .select-tree-common .content
right: 46px
&.navigated .select-tree-common .content
animation-name: selectTreeCommonCloseRight
.tree-inputs>label
float: left
width: 50%
@keyframes selectTreeCommonCloseRight {
from {
height: 37px
display: table
right: 6px
opacity: 1
visibility: visible
}
to {
height: 100%
right: -100%
opacity: 0
visibility: hidden
width: calc(100% - 6px)
}
}