chore(reorganization) Moving longitudinal feature to Lesiontracker package (#208)

This commit is contained in:
Gustavo André Lelis authored and Erik Ziegler committed 2018-06-18 18:03:34 +02:00
1 parent 1643e9f7d6
commit 9f73ae0153
11 files changed
+2 -1

No files matched your search

@@ -8,3 +8,5 @@ import './trialOptionsModal/recistDescription.html';
import './trialOptionsModal/trialOptionsModal.html';
import './trialOptionsModal/trialOptionsModal.js';
import './longitudinal';
@@ -0,0 +1,110 @@
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core';
/**
* Loads multiple unassociated studies in the Viewer
*/
const getAssociationAssessment = () => {
// default result value
const assessment = {
selected: 0,
associated: 0
};
// check if timepointApi is available
const timepointApi = OHIF.studylist.timepointApi;
if (timepointApi) {
// Get a Cursor pointing to the selected Studies from the StudyList
const selectedStudies = OHIF.studylist.getSelectedStudies();
if (selectedStudies.length > 0) {
assessment.selected = selectedStudies.length;
// Loop through the selected Studies and return true if at least one study has no association.
for (let i = selectedStudies.length - 1; i >= 0; --i) {
let study = selectedStudies[i],
timepoints = timepointApi.study(study.studyInstanceUid);
if (timepoints && timepoints.length > 0) {
assessment.associated++;
}
}
}
}
return assessment;
};
/**
* Removes all present study / timepoint associations from the Clinical Trial
*/
const removeTimepointAssociations = event => {
const dialogSettings = {
title: 'Remove Association',
message: 'Measurements related to this Study and Timepoint will be erased. Do you really want to delete this association?',
confirmClass: 'btn-danger',
position: {
x: event.clientX,
y: event.clientY
}
};
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
// Get a Cursor pointing to the selected Studies from the StudyList
const selectedStudies = OHIF.studylist.getSelectedStudies();
// Find the Timepoint that was previously referenced
const timepointApi = OHIF.studylist.timepointApi;
if (!timepointApi) {
OHIF.log.error('Remove Study/Timepoint Association: No Timepoint API found.');
return;
}
// Loop through the Cursor of Selected Studies
selectedStudies.forEach(study => {
const studyInstanceUid = study.studyInstanceUid;
const timepoints = timepointApi.study(studyInstanceUid);
const timepointIds = timepoints.map(t => t.timepointId);
timepointApi.disassociateStudy(timepointIds, studyInstanceUid);
});
});
};
Meteor.startup(() => {
if (!OHIF.studylist) return;
OHIF.studylist.dropdown.setItems([{
action: OHIF.studylist.viewStudies,
text: 'View',
separatorAfter: true
}, {
action: () => OHIF.ui.showDialog('dialogStudyAssociation'),
text: 'Associate',
disabled: () => {
const assessment = getAssociationAssessment();
return assessment.selected < 1;
}
}, {
action: removeTimepointAssociations,
text: 'Remove Association',
separatorAfter: true,
disabled: () => {
const assessment = getAssociationAssessment();
return assessment.selected < 1 || assessment.selected !== assessment.associated;
}
}, {
action: OHIF.studylist.viewSeriesDetails,
text: 'View Series Details'
}, {
text: 'Anonymize',
disabled: true
}, {
text: 'Send',
disabled: true,
separatorAfter: true
}, {
action: OHIF.studylist.exportSelectedStudies,
text: 'Export',
title: 'Export Selected Studies'
}, {
text: 'Delete',
disabled: true
}]);
});
@@ -0,0 +1,11 @@
// Longitudinal Components imports
import './longitudinalStudyListStudy/longitudinalStudyListStudy.html';
import './longitudinalStudyListStudy/longitudinalStudyListStudy.styl';
import './longitudinalStudyListStudy/longitudinalStudyListStudy.js';
import './longitudinalViewportOverlay/imageViewportIcons.html';
import './longitudinalViewportOverlay/longitudinalViewportOverlay.html';
import './longitudinalViewportOverlay/longitudinalViewportOverlay.js';
import './longitudinalViewportOverlay/longitudinalViewportOverlay.styl';
import './dropdown.js';
@@ -0,0 +1,28 @@
<template name="longitudinalStudyListStudy">
<tr class="studylistStudy noselect {{#if this.selected}}active{{/if}}">
<td>
{{#if reviewerTip}}
<i class="fa fa-exclamation-circle reviewerTip" data-toggle="tooltip" data-placement="bottom" title="{{reviewerTip}}"></i>
{{/if}}
{{formatPN patientName}}
</td>
<td>
{{patientId}}
</td>
<td>
{{accessionNumber}}
</td>
<td>
{{formatDA studyDate}} {{ #if timepointName }}({{timepointName}}){{ /if }}
</td>
<td>
{{modalities}}
</td>
<td>
{{studyDescription}}
</td>
<td>
{{numberOfStudyRelatedInstances}}
</td>
</tr>
</template>
@@ -0,0 +1,80 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { OHIF } from 'meteor/ohif:core';
// Use Aldeed's meteor-template-extension package to replace the
// default StudyListStudy template.
// See https://github.com/aldeed/meteor-template-extension
const defaultTemplate = 'studylistStudy';
if (OHIF.studylist) {
Template.longitudinalStudyListStudy.replaces(defaultTemplate);
// Add the TimepointName helper to the default template. The
// HTML of this template is replaced with that of longitudinalStudyListStudy
Template[defaultTemplate].helpers({
timepointName() {
const instance = Template.instance();
const timepointApi = OHIF.studylist.timepointApi;
if (!timepointApi) {
return;
}
const timepoint = timepointApi.study(instance.data.studyInstanceUid)[0];
if (!timepoint) {
return;
}
return timepointApi.name(timepoint);
},
reviewerTip() {
const instance = Template.instance();
const timepointApi = OHIF.studylist.timepointApi;
if (!timepointApi || !window.Reviewers) {
return;
}
const timepoint = timepointApi.study(instance.data.studyInstanceUid);
if (!timepoint) {
return;
}
const timepointReviewers = window.Reviewers.findOne({ timepointId: timepoint.timepointId });
if (!timepointReviewers) {
return;
}
return getReviewerTipText(timepointReviewers.reviewers);
}
});
function getReviewerTipText(reviewers) {
if (!reviewers || !reviewers.length) {
return;
}
const newReviewers = reviewers.filter(function(reviewer) {
return reviewer.userId !== Meteor.userId();
});
if (!newReviewers.length) {
return;
}
let tipText = 'The study is being reviewed by ';
newReviewers.forEach(function(reviewer, index) {
if (reviewer.userId === Meteor.userId()) {
return;
}
if (index > 0) {
tipText += ',';
}
tipText += reviewer.userName;
});
return tipText;
}
}
@@ -0,0 +1,5 @@
.studylistStudy
cursor: pointer
.reviewerTip
color: darkred
@@ -0,0 +1 @@
<template name="imageViewportIcons"></template>
@@ -0,0 +1,84 @@
<template name="longitudinalViewportOverlay">
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
{{#unless tagDisplaySpecified}}
<div class="topleft dicomTag">
<div>{{patientId}}</div>
<div>{{patientName}}</div>
<div>{{getGenderAndAge}}</div>
<div class="icons-section">
{{#if linked}}
<svg class="icon-link">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-viewport-link"}}></use>
</svg>
{{/if}}
{{>imageViewportIcons this}}
</div>
</div>
<div class="topright dicomTag">
<div>{{seriesDescription}}</div>
<div>{{studyInfo 'AccessionNumber'}}</div>
<div>{{formatDA (studyInfo 'AcquisitionDate')}} {{formatTM (studyInfo 'AcquisitionTime')}}</div>
<div>{{#if seriesNumber}}S: {{seriesNumber}}{{#if gt numImages 1}},{{/if}} {{/if}}{{#if gt numImages 1}}I: {{imageIndex}}/{{numImages}}{{/if}}</div>
<div class='timepointName'>{{timepointName}}</div>
</div>
<div class="bottomright dicomTag">
<div class="stationName">{{studyInfo 'StationName'}}</div>
<div class="manufacturerModel">{{studyInfo 'ManufacturersModelName'}}</div>
<div class="institutionName">{{studyInfo 'InstitutionName'}}</div>
</div>
<div class="bottomleft dicomTag">
<div>
<span>{{#if location}}Loc: {{formatNumberPrecision location 2}} mm{{/if}}</span>
<span>{{#if thickness}}Thick: {{formatNumberPrecision thickness 2}} mm{{/if}}</span>
<span>{{#if spacingBetweenSlices}}Spacing: {{formatNumberPrecision spacingBetweenSlices 2}} mm{{/if}}</span>
</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}x{{/if}}</div>
<div>{{wwwc}}</div>
</div>
{{/unless}}
{{#if tagDisplayRightOnly}}
<div class="topright dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='timepointName'>{{timepointName}}</div>
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div class="compressionIndicator">{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{/if}}
{{#if tagDisplayLeftOnly}}
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='timepointName'>{{timepointName}}</div>
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomleft dicomTag">
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
<div class="compressionIndicator">{{compression}}</div>
<div>{{wwwc}}</div>
</div>
{{/if}}
{{#if gt numImages 1}}
{{>imageControls imageIndex=imageIndex numImages=numImages}}
{{/if}}
</div>
</template>
@@ -0,0 +1,179 @@
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
import { moment } from 'meteor/momentjs:moment';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone } from 'meteor/ohif:cornerstone';
// Use Aldeed's meteor-template-extension package to replace the
// default viewportOverlay template.
// See https://github.com/aldeed/meteor-template-extension
const defaultTemplate = 'viewportOverlay';
Template.longitudinalViewportOverlay.replaces(defaultTemplate);
Template[defaultTemplate].onCreated(() => {
const instance = Template.instance();
instance.instanceMetadata = new ReactiveVar();
const { DICOMTagDescriptions } = OHIF.viewerbase;
instance.getValueByTagKeyword = tagKeyword => {
const instanceMetadata = instance.instanceMetadata.get();
const tagObject = DICOMTagDescriptions.find(tagKeyword);
if (!instanceMetadata || !tagObject) return;
return instanceMetadata.getRawValue(tagObject.tag);
};
});
Template[defaultTemplate].onRendered(() => {
const instance = Template.instance();
const { studyInstanceUid, seriesInstanceUid } = instance.data;
Tracker.autorun(computation => {
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
if (computation.firstRun) return;
computation.stop();
const imageIndex = instance.getImageIndex();
OHIF.studies.loadStudy(studyInstanceUid).then(studyMetadata => {
const seriesMetadata = studyMetadata.getSeriesByUID(seriesInstanceUid);
const instanceMetadata = seriesMetadata.getInstanceByIndex(imageIndex);
if (!instanceMetadata) return;
instance.instanceMetadata.set(instanceMetadata);
});
});
});
// Add the TimepointName helper to the default template. The
// HTML of this template is replaced with that of longitudinalViewportOverlay
Template[defaultTemplate].helpers({
studyInfo(tagKeyword) {
const instance = Template.instance();
instance.instanceMetadata.dep.depend();
return instance.getValueByTagKeyword(tagKeyword);
},
getGenderAndAge() {
const instance = Template.instance();
const values = [];
values.push(instance.getValueByTagKeyword('PatientSex'));
const patientAge = instance.getValueByTagKeyword('PatientAge');
const patientBirthDate = instance.getValueByTagKeyword('PatientBirthDate');
if (patientAge) {
values.push(patientAge);
} else if (patientBirthDate) {
const date = moment(patientBirthDate, 'YYYYMMDD');
const yearDiff = moment().diff(date, 'years');
if (yearDiff) {
values.push((yearDiff + 'Y').padStart(4, '0'));
} else {
const monthDiff = moment().diff(date, 'months');
if (monthDiff) {
values.push((monthDiff + 'M').padStart(4, '0'));
} else {
const dayDiff = moment().diff(date, 'days') || 0;
values.push((dayDiff + 'D').padStart(4, '0'));
}
}
}
return values.filter(value => !!value).join(', ');
},
thickness() {
const instance = Template.instance();
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
return instance.getValueByTagKeyword('SliceThickness');
},
location() {
const instance = Template.instance();
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
const sliceLocation = instance.getValueByTagKeyword('SliceLocation');
const tablePosition = instance.getValueByTagKeyword('TablePosition');
const imagePositionPatient = instance.getValueByTagKeyword('ImagePositionPatient');
return sliceLocation || tablePosition || imagePositionPatient;
},
spacingBetweenSlices() {
const instance = Template.instance();
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
// TODO: Otherwise, displays a value derived from successive values
// of Image Position (Patient) (0020,0032) perpendicular to
// the Image Orientation (Patient) (0020,0037)
return instance.getValueByTagKeyword('SpacingBetweenSlices');
},
zoom() {
const instance = Template.instance();
const { viewportIndex } = instance.data;
const { getElementIfNotEmpty } = OHIF.viewerbase;
Session.get('CornerstoneImageRendered' + viewportIndex);
const element = getElementIfNotEmpty(viewportIndex);
if (!element) return;
const viewport = cornerstone.getViewport(element);
if (!viewport) return;
return viewport.scale;
},
wwwc() {
const instance = Template.instance();
const { viewportIndex } = instance.data;
const { getElementIfNotEmpty, wlPresets } = OHIF.viewerbase;
Session.get('CornerstoneImageRendered' + viewportIndex);
wlPresets.changeObserver.depend();
const element = getElementIfNotEmpty(viewportIndex);
if (!element) return;
const viewport = cornerstone.getViewport(element);
if (!viewport) return;
const ww = viewport.voi.windowWidth.toFixed(0);
const wc = viewport.voi.windowCenter.toFixed(0);
const result = [`W: ${ww}, L: ${wc}`];
// Check if there's a preset with this W/L
const preset = _.findWhere(OHIF.viewer.wlPresets, {
ww: parseInt(ww),
wc: parseInt(wc)
});
// Append the preset name to the result if found
if (preset) {
result .push(`(${preset.id})`);
}
return result.join(' ');
},
timepointName() {
const instance = Template.instance();
const studyInstanceUid = instance.data.studyInstanceUid;
const timepointApi = OHIF.viewer.timepointApi;
if (!timepointApi) return;
const timepoints = timepointApi.study(studyInstanceUid);
if (!timepoints || !timepoints.length) {
return;
}
const timepoint = timepoints[0];
return timepointApi.name(timepoint);
},
linked() {
const linkedViewports = Session.get('StackImagePositionOffsetSynchronizerLinkedViewports') || [];
return (linkedViewports.indexOf(this.viewportIndex) !== -1);
}
});
@@ -0,0 +1,4 @@
@require '{ohif:design}/app'
.imageViewerViewportOverlay .icons-section .icon-link
transform(translateY(3px))