WIP reorganization
This commit is contained in:
1 parent
6706dfe634
commit
054e37dfb7
266 files changed
+98
-620
No files matched your search
@@ -0,0 +1,16 @@
|
||||
<template name="dialogNonTargetMeasurement">
|
||||
{{#dialogForm (extend this
|
||||
dialogClass='modal-sm'
|
||||
schema=(choose this.schema instance.schema)
|
||||
api=instance.api
|
||||
)}}
|
||||
{{>inputSelect labelClass='form-group' key='location' hideSearch=true
|
||||
options=(clone placeholder='Choose a lesion location')}}
|
||||
{{>inputSelect labelClass='form-group' key='response' hideSearch=true
|
||||
options=(clone placeholder='Choose a lesion location response')}}
|
||||
{{#if this.edit}}
|
||||
<hr>
|
||||
{{#button class='btn btn-danger' action='remove'}}Remove{{/button}}
|
||||
{{/if}}
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
Template.dialogNonTargetMeasurement.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.measurementTypeId = 'nonTarget';
|
||||
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
instance.schema = new SimpleSchema({
|
||||
location: config.schema.nonTargetLocation,
|
||||
response: config.schema.nonTargetResponse
|
||||
});
|
||||
|
||||
// Remove the measurement from the collection
|
||||
instance.removeMeasurement = () => {
|
||||
const measurementApi = instance.viewerData.measurementApi;
|
||||
measurementApi.deleteMeasurements('nonTargets', {
|
||||
toolItemId: instance.data.measurementData._id
|
||||
});
|
||||
|
||||
// Sync the new measurement data with cornerstone tools
|
||||
|
||||
// Commenting this out for now, we need the timepointApi
|
||||
//const baseline = timepointApi.baseline();
|
||||
//measurementApi.sortMeasurements(baseline.timepointId);
|
||||
|
||||
// Refresh the image with the measurement removed
|
||||
cornerstone.updateImage(instance.data.element);
|
||||
};
|
||||
|
||||
// Close the current dialog
|
||||
instance.closeDialog = () => instance.$('.form-action.close').trigger('click');
|
||||
|
||||
instance.api = {
|
||||
// Confirm the deletion of the current non-target measurement
|
||||
remove() {
|
||||
const dialogSettings = {
|
||||
position: instance.data.position,
|
||||
title: 'Remove Measurement',
|
||||
message: 'Are you sure you want to remove this Non-Target measurement?'
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings)
|
||||
.then(instance.removeMeasurement);
|
||||
|
||||
instance.closeDialog();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.dialogNonTargetMeasurement.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
const form = instance.$('form').data('component');
|
||||
|
||||
const viewerMain = $(instance.data.element).closest('.viewerMain')[0];
|
||||
instance.viewerData = Blaze.getData(viewerMain);
|
||||
|
||||
const measurementApi = instance.viewerData.measurementApi;
|
||||
const timepointApi = instance.viewerData.timepointApi;
|
||||
|
||||
const collection = measurementApi.tools[instance.measurementTypeId];
|
||||
|
||||
const measurementData = instance.data.measurementData;
|
||||
|
||||
// Get the current inserted measurement from the collection
|
||||
const currentMeasurement = collection.findOne({ _id: measurementData._id });
|
||||
|
||||
// Check if it's a edition or creation
|
||||
if (instance.data.edit) {
|
||||
// Set the data that is already defined for current measurement
|
||||
form.value(currentMeasurement);
|
||||
} else {
|
||||
// LT-112 Non-target response shall default to non-measurable on baseline, present on follow-up
|
||||
const timepoint = timepointApi.study(measurementData.studyInstanceUid)[0];
|
||||
const response = timepoint && timepoint.timepointType === 'baseline' ? 'Present' : '';
|
||||
|
||||
// Get a previously inserted Non-target
|
||||
const previousMeasurement = collection.findOne({
|
||||
_id: { $not: measurementData._id },
|
||||
measurementNumber: currentMeasurement.measurementNumber
|
||||
});
|
||||
|
||||
// Change the location for current measurement if it's not the first one
|
||||
const location = previousMeasurement && previousMeasurement.location;
|
||||
|
||||
// Set the default location and response
|
||||
form.value({
|
||||
location,
|
||||
response
|
||||
});
|
||||
|
||||
// Synchronize the measurement number with the one inserted in the collection
|
||||
measurementData.measurementNumber = currentMeasurement.measurementNumber;
|
||||
|
||||
// Refresh the image with the measurement number
|
||||
cornerstone.updateImage(instance.data.element);
|
||||
}
|
||||
|
||||
// Delete the measurement from collection when dialog is closed and not on edit mode
|
||||
instance.data.promise.catch(() => {
|
||||
if (instance.data.edit) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.removeMeasurement();
|
||||
});
|
||||
|
||||
// Update the location and response after confirming the dialog data
|
||||
instance.data.promise.then(formData => {
|
||||
measurementData.response = formData.response;
|
||||
measurementData.location = formData.location;
|
||||
|
||||
// Update the response for current measurement
|
||||
collection.update({
|
||||
_id: measurementData._id,
|
||||
}, {
|
||||
$set: { response: formData.response }
|
||||
});
|
||||
|
||||
// Change the location for all Non-Target measurements with the same measurementNumber
|
||||
collection.update({
|
||||
measurementNumber: currentMeasurement.measurementNumber
|
||||
}, {
|
||||
$set: { location: formData.location }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import './dialog/nonTargetMeasurement.html';
|
||||
import './dialog/nonTargetMeasurement.js';
|
||||
|
||||
import './timepointBrowser';
|
||||
|
||||
import './trialOptionsModal/irRCDescription.html';
|
||||
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';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<template name="longitudinalStudyListStudy">
|
||||
<tr class="studylistStudy noselect {{#if this.selected}}active{{/if}}">
|
||||
<td>
|
||||
{{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>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
.studylistStudy
|
||||
cursor: pointer
|
||||
+1
@@ -0,0 +1 @@
|
||||
<template name="imageViewportIcons"></template>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<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="topcenter dicomTag">
|
||||
<div class='timepointName'>{{timepointName}}</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>
|
||||
<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>
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
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;
|
||||
|
||||
instance.autorun(computation => {
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
if (computation.firstRun) return;
|
||||
computation.stop();
|
||||
const imageIndex = instance.getImageIndex();
|
||||
|
||||
if (!studyInstanceUid || !seriesInstanceUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
OHIF.studies.loadStudy(studyInstanceUid).then(study => {
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
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);
|
||||
}
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.imageViewerViewportOverlay .icons-section .icon-link
|
||||
transform(translateY(3px))
|
||||
@@ -0,0 +1,10 @@
|
||||
import './item.html';
|
||||
import './item.js';
|
||||
import './list.html';
|
||||
import './list.styl';
|
||||
import './quickSwitch.html';
|
||||
import './quickSwitch.js';
|
||||
import './sidebar.html';
|
||||
import './sidebar.js';
|
||||
import './studies.html';
|
||||
import './studies.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
<template name="timepointBrowserItem">
|
||||
<div class="timepoint-browser-item" data-id="{{this.timepoint.timepointId}}">
|
||||
<div class="timepoint-item p-y-2">
|
||||
<div class="timepoint-details clearfix">
|
||||
<div class="timepoint-title pull-left">
|
||||
{{this.timepointApi.title timepoint}}
|
||||
</div>
|
||||
<div class="timepoint-expand-icon pull-right">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="timepoint-date pull-right m-r-1">{{formatDA this.timepoint.earliestDate 'D-MMM-YYYY'}}</div>
|
||||
</div>
|
||||
<div class="timepoint-summary">{{choose instance.summary.get 'Click to load'}}</div>
|
||||
</div>
|
||||
{{#if this.timepointChildTemplate}}
|
||||
{{#if instance.studiesData.get}}
|
||||
{{>Template.dynamic template=this.timepointChildTemplate data=(clone this studiesInformation=instance.studiesData.get)}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.timepointBrowserItem.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { timepoint, timepointApi } = instance.data;
|
||||
const { timepointId } = timepoint;
|
||||
|
||||
const hasStudiesData = !!(timepoint.studiesData && timepoint.studiesData.length);
|
||||
instance.summary = new ReactiveVar('');
|
||||
instance.studiesData = new ReactiveVar(hasStudiesData ? timepoint.studiesData : null);
|
||||
|
||||
const updateStudiesData = newDocument => {
|
||||
const newTimepoint = newDocument || timepointApi.timepoints.findOne({ timepointId });
|
||||
if (newTimepoint && newTimepoint.studiesData && newTimepoint.studiesData.length) {
|
||||
instance.studiesData.set(newTimepoint.studiesData);
|
||||
}
|
||||
};
|
||||
|
||||
timepointApi.timepoints.find({ timepointId }).observe({ changed: updateStudiesData });
|
||||
|
||||
// Build the modalities summary of all timepoint's studies
|
||||
instance.setModalitiesSummary = () => {
|
||||
const studiesData = instance.studiesData.get();
|
||||
if (!studiesData) return;
|
||||
|
||||
const modalities = {};
|
||||
studiesData.forEach(study => {
|
||||
const modality = study.modalities || 'UN';
|
||||
modalities[modality] = modalities[modality] + 1 || 1;
|
||||
});
|
||||
|
||||
const summary = [];
|
||||
_.each(modalities, (count, modality) => summary.push(`${count} ${modality}`));
|
||||
|
||||
instance.summary.set(summary.join(', '));
|
||||
};
|
||||
|
||||
const filter = { studyInstanceUid: timepoint.studyInstanceUids };
|
||||
instance.loadStudies = () => OHIF.studies.searchStudies(filter).then(studiesData => {
|
||||
instance.studiesData.set(studiesData);
|
||||
timepointApi.timepoints.update(timepoint._id, { $set: { studiesData } });
|
||||
instance.setModalitiesSummary();
|
||||
}).catch(error => {
|
||||
const text = 'An error has occurred while retrieving studies information';
|
||||
OHIF.ui.notifications.danger({ text });
|
||||
OHIF.log.error(error);
|
||||
instance.summary.set('Failed');
|
||||
});
|
||||
|
||||
updateStudiesData();
|
||||
instance.autorun(() => {
|
||||
const studiesData = instance.studiesData.get();
|
||||
if (studiesData) {
|
||||
instance.setModalitiesSummary();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserItem.events({
|
||||
'ohif.measurements.timepoint.load .timepoint-item'(event, instance) {
|
||||
instance.loadStudies();
|
||||
},
|
||||
|
||||
'click .timepoint-item'(event, instance) {
|
||||
const element = event.currentTarget.parentElement;
|
||||
const $element = $(element);
|
||||
|
||||
const triggerClick = () => {
|
||||
$element.trigger('ohif.measurements.timepoint.click', instance.data.timepoint);
|
||||
};
|
||||
|
||||
if (!instance.studiesData.get()) {
|
||||
instance.summary.set('Loading...');
|
||||
instance.loadStudies().then(() => Tracker.afterFlush(triggerClick));
|
||||
} else {
|
||||
triggerClick();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<template name="timepointBrowserList">
|
||||
<div class="timepoint-browser-list {{this.class}}">
|
||||
{{#each timepoint in this.timepoints}}
|
||||
{{>timepointBrowserItem (clone this timepoint=timepoint)}}
|
||||
<hr>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.timepoint-switch
|
||||
theme('border-bottom', '1px solid $uiBorderColor')
|
||||
|
||||
.timepoint-browser-item
|
||||
|
||||
.timepoint-item
|
||||
cursor: pointer
|
||||
font-size: 13px
|
||||
|
||||
&:not(.active) .timepoint-browser-studies
|
||||
max-height: 0 !important
|
||||
|
||||
.timepoint-studies-container
|
||||
opacity: 0
|
||||
transform(translateY(-100%))
|
||||
|
||||
.timepoint-browser-studies
|
||||
overflow: hidden
|
||||
transition(max-height 0.3s ease)
|
||||
|
||||
.timepoint-studies-container
|
||||
opacity: 1
|
||||
transition(opacity 0.3s ease\, transform 0.3s ease)
|
||||
transform(translateY(0))
|
||||
transform-origin(50% 0%)
|
||||
|
||||
.timepoint-date
|
||||
opacity: 1
|
||||
|
||||
&.active
|
||||
.timepoint-summary
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
|
||||
.timepoint-date
|
||||
opacity: 0
|
||||
|
||||
.timepoint-expand-icon i
|
||||
transform: rotateX(180deg)
|
||||
|
||||
.timepoint-title
|
||||
font-size: 14px
|
||||
theme('color', '$textSecondaryColor')
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
.timepoint-expand-icon i,
|
||||
.timepoint-title,
|
||||
.timepoint-date,
|
||||
.timepoint-summary
|
||||
theme('color', '$textSecondaryColor')
|
||||
transition($sidebarTransition)
|
||||
|
||||
&:hover
|
||||
&,
|
||||
.timepoint-title,
|
||||
.timepoint-expand-icon i,
|
||||
.timepoint-date,
|
||||
.timepoint-summary
|
||||
theme('color', '$textPrimaryColor') !important
|
||||
|
||||
.timepoint-details
|
||||
.timepoint-summary
|
||||
line-height: 25px
|
||||
|
||||
.timepoint-summary
|
||||
height: 25px
|
||||
max-height: 25px
|
||||
opacity: 1
|
||||
overflow: hidden
|
||||
|
||||
.timepoint-expand-icon i
|
||||
theme('color', '$defaultColor')
|
||||
|
||||
.timepoint-title
|
||||
theme('color', '$textPrimaryColor')
|
||||
padding-top: 2px
|
||||
|
||||
.series-quick-switch .timepoint-browser-list
|
||||
padding: 0 10px
|
||||
|
||||
div.timepoint-title
|
||||
text-transform: none
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="timepointBrowserQuickSwitch">
|
||||
{{>timepointBrowserList (extend timepointBrowserData)}}
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.timepointBrowserQuickSwitch.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = OHIF.viewer;
|
||||
|
||||
instance.selectedTimepoint = new ReactiveVar();
|
||||
instance.timepoints = new ReactiveVar([]);
|
||||
|
||||
instance.updateSelectedTimepoint = studyInstanceUid => {
|
||||
const selectedTimepoint = timepointApi.study(studyInstanceUid)[0];
|
||||
instance.selectedTimepoint.set(selectedTimepoint);
|
||||
};
|
||||
|
||||
const currentTimepoint = timepointApi.current();
|
||||
const filter = { latestDate: { $lte: currentTimepoint.latestDate } };
|
||||
instance.keyTimepoints = timepointApi.key(filter);
|
||||
|
||||
const { viewportIndex } = instance.data;
|
||||
instance.autorun(() => {
|
||||
OHIF.viewerbase.layoutManager.observer.depend();
|
||||
const viewportData = OHIF.viewerbase.layoutManager.viewportData[viewportIndex];
|
||||
let { studyInstanceUid } = viewportData;
|
||||
if (!studyInstanceUid) {
|
||||
Tracker.nonreactive(() => {
|
||||
const currentStudy = instance.data.currentStudy.get();
|
||||
if (currentStudy) {
|
||||
studyInstanceUid = currentStudy.studyInstanceUid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.updateSelectedTimepoint(studyInstanceUid);
|
||||
});
|
||||
|
||||
instance.autorun(() => {
|
||||
const selectedTimepoint = instance.selectedTimepoint.get();
|
||||
const timepoints = [selectedTimepoint];
|
||||
instance.timepoints.set(timepoints);
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.updateActiveStudy = () => {
|
||||
const currentStudy = instance.data.currentStudy.get();
|
||||
const studyInstanceUid = (currentStudy && currentStudy.studyInstanceUid) || '';
|
||||
Tracker.afterFlush(() => {
|
||||
const $studyBrowserItems = instance.$('.study-browser-item');
|
||||
$studyBrowserItems.removeClass('active');
|
||||
$studyBrowserItems.filter(`[data-uid="${studyInstanceUid}"]`).addClass('active');
|
||||
});
|
||||
};
|
||||
|
||||
instance.autorun(() => {
|
||||
const selectedTimepoint = instance.selectedTimepoint.get();
|
||||
const selectedTimepointId = (selectedTimepoint && selectedTimepoint.timepointId) || '';
|
||||
const $allBrowserItems = instance.$('.timepoint-browser-item');
|
||||
const $browserItem = $allBrowserItems.filter(`[data-id=${selectedTimepointId}]`);
|
||||
if (!$browserItem.hasClass('active')) {
|
||||
$browserItem.find('.timepoint-item').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
instance.autorun(instance.updateActiveStudy);
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.events({
|
||||
'ohif.measurements.timepoint.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
$element.toggleClass('active');
|
||||
instance.updateActiveStudy();
|
||||
},
|
||||
|
||||
'ohif.studies.study.click'(event, instance, studyInformation) {
|
||||
const { studyInstanceUid } = studyInformation;
|
||||
const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
|
||||
instance.data.currentStudy.set(study);
|
||||
const $studySwitch = $(event.currentTarget).closest('.study-switch');
|
||||
$studySwitch.siblings('.series-switch').trigger('rescale');
|
||||
instance.updateSelectedTimepoint(studyInformation.studyInstanceUid);
|
||||
|
||||
// Create a hover bridge to prevent quick switch from closing due to scroll height
|
||||
Meteor.defer(() => {
|
||||
const $scrollable = $studySwitch.find('.study-browser>.scrollable');
|
||||
const offsetY = $scrollable.offset().top + $scrollable.outerHeight();
|
||||
if (event.clientY > offsetY) {
|
||||
const hoverHandler = _.throttle(event => {
|
||||
if (event.clientY <= offsetY) {
|
||||
$scrollable.css('padding-bottom', '');
|
||||
$scrollable.off('mousemove', hoverHandler);
|
||||
$scrollable.off('mouseleave', hoverHandler);
|
||||
}
|
||||
}, 100);
|
||||
$scrollable.css('padding-bottom', event.clientY - offsetY + 20);
|
||||
$scrollable.on('mousemove', hoverHandler);
|
||||
$scrollable.one('mouseleave', () => $scrollable.off('mousemove', hoverHandler));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.helpers({
|
||||
timepointBrowserData() {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = OHIF.viewer;
|
||||
return {
|
||||
timepointApi,
|
||||
timepoints: instance.timepoints.get(),
|
||||
timepointChildTemplate: 'timepointBrowserStudies'
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<template name="timepointBrowserSidebar">
|
||||
<div class="full-height flex-v noselect">
|
||||
<div class="p-x-1">
|
||||
<div class="timepoint-switch p-y-2">
|
||||
{{>roundedButtonGroup viewTypeButtonGroupData}}
|
||||
</div>
|
||||
</div>
|
||||
{{#scrollArea class='flex-grow fit' scrollableClass='p-a-1' scrollStep=191}}
|
||||
{{>timepointBrowserList (extend timepointBrowserData)}}
|
||||
{{/scrollArea}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
Template.timepointBrowserSidebar.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Reactive variable to control the view type for all or key timepoints
|
||||
instance.timepointViewType = new ReactiveVar(instance.data.timepointViewType);
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.lastType = '';
|
||||
|
||||
// Collapse all timepoints but first when timepoint view type changes
|
||||
instance.$browserList = instance.$('.timepoint-browser-list').first();
|
||||
instance.autorun(() => {
|
||||
// Runs this computation every time the timepointViewType is changed
|
||||
const type = instance.timepointViewType.get();
|
||||
if (type !== instance.lastType) {
|
||||
const eventKey = 'ohif.measurements.timepoint.changeViewType';
|
||||
instance.$browserList.trigger(eventKey, type);
|
||||
}
|
||||
|
||||
instance.lastType = type;
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.events({
|
||||
'ohif.studies.study.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
|
||||
// Defer the active class toggling to wait for child template rendering
|
||||
Meteor.defer(() => {
|
||||
// Remove max height restriction from studies browser
|
||||
const $studiesBrowser = $element.closest('.timepoint-browser-studies');
|
||||
$studiesBrowser.css('max-height', '');
|
||||
|
||||
// Remove active class from sibling studies
|
||||
$element.siblings().removeClass('active');
|
||||
|
||||
// Toggle the active class on clicked study
|
||||
$element.toggleClass('active');
|
||||
|
||||
// Adjust the max height for studiesBrowser when series transition is finished
|
||||
const $seriesBrowser = $element.find('.study-browser-series');
|
||||
$seriesBrowser.one('transitionend', () => $studiesBrowser.adjustMax('height'));
|
||||
});
|
||||
},
|
||||
|
||||
'ohif.measurements.timepoint.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
|
||||
// Defer the active class toggling to wait for child template rendering
|
||||
Meteor.defer(() => $element.toggleClass('active'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.helpers({
|
||||
viewTypeButtonGroupData() {
|
||||
return {
|
||||
value: Template.instance().timepointViewType,
|
||||
options: [{
|
||||
value: 'key',
|
||||
text: 'Key Timepoints'
|
||||
}, {
|
||||
value: 'all',
|
||||
text: 'All Timepoints'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
timepointBrowserData() {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = instance.data;
|
||||
|
||||
const currentTimepoint = timepointApi.current();
|
||||
const { patientId } = currentTimepoint;
|
||||
let timepoints = [];
|
||||
if (instance.timepointViewType.get() === 'key') {
|
||||
const filter = { latestDate: { $lte: currentTimepoint.latestDate } };
|
||||
timepoints = timepointApi.key(filter);
|
||||
} else {
|
||||
timepoints = timepointApi.all({ patientId });
|
||||
}
|
||||
|
||||
return {
|
||||
timepointApi,
|
||||
timepoints,
|
||||
timepointChildTemplate: 'timepointBrowserStudies',
|
||||
studyChildTemplate: 'studyBrowserSeries'
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="timepointBrowserStudies">
|
||||
<div class="timepoint-browser-studies">
|
||||
<div class="timepoint-studies-container">
|
||||
{{>studyBrowserList this}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.timepointBrowserStudies.onRendered(() => {
|
||||
Template.instance().$('.timepoint-browser-studies').adjustMax('height');
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<template name="irRCDescription">
|
||||
<h4>irRC</h4>
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Target lesions must be >= 10 X 10 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<h5>New Lesion Checks for Follow-ups</h5>
|
||||
<ul>
|
||||
<li>New target lesions must be >= 5 X 5 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
</ul>
|
||||
<!--Time Point Measurement Total = SPD target lesions + SPD new lesions (SPD = sum of product of long axis and short axis diameters)-->
|
||||
</template>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<template name="recistDescription">
|
||||
<h4>RECIST 1.1</h4>
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Extranodal lesions must be >= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)</li>
|
||||
<li>Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 2 target lesions per organ</li>
|
||||
<li>Up to a max of 5 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<!-- Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions</li>-->
|
||||
</template>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<template name="trialOptionsModal">
|
||||
{{#dialogForm (extend this
|
||||
schema=instance.schema dialogClass='modal-lg'
|
||||
confirmLabel='Save' title='Clinical Trial Options'
|
||||
)}}
|
||||
<h3 class="m-t-0">Select one or more Trial Criteria types:</h3>
|
||||
{{>groupRadio class='js-trial' labelClass='form-group' key='trialCriteria'}}
|
||||
|
||||
<h3>Selected Trial Criteria:</h3>
|
||||
<div class="m-t-2">
|
||||
{{#let descriptionTemplate=(getDescriptionTemplate instance.selectedTrial.get)}}
|
||||
{{>Template.dynamic template=descriptionTemplate}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
Meteor.startup(() => {
|
||||
const TrialCriteriaTypes = new Meteor.Collection(null);
|
||||
TrialCriteriaTypes._debugName = 'TrialCriteriaTypes';
|
||||
|
||||
TrialCriteriaTypes.insert({
|
||||
id: 'RECIST',
|
||||
name: 'RECIST 1.1',
|
||||
descriptionTemplate: 'recistDescription',
|
||||
selected: true
|
||||
});
|
||||
|
||||
TrialCriteriaTypes.insert({
|
||||
id: 'irRC',
|
||||
name: 'irRC',
|
||||
descriptionTemplate: 'irRCDescription',
|
||||
selected: false
|
||||
});
|
||||
|
||||
OHIF.lesiontracker.TrialCriteriaTypes = TrialCriteriaTypes;
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { TrialCriteriaTypes } = OHIF.lesiontracker;
|
||||
const types = TrialCriteriaTypes.find().fetch();
|
||||
const defaultValue = _.findWhere(types, { selected: true })._id;
|
||||
instance.selectedTrial = new ReactiveVar(defaultValue);
|
||||
|
||||
instance.schema = new SimpleSchema({
|
||||
trialCriteria: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(types, '_id'),
|
||||
valuesLabels: _.pluck(types, 'name'),
|
||||
defaultValue
|
||||
}
|
||||
});
|
||||
|
||||
instance.data.promise.then(formData => {
|
||||
// Set "selected" to false for the entire collection
|
||||
TrialCriteriaTypes.update({}, {
|
||||
$set: { selected: false }
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// TODO: Use filter with "_id: $in" when allowing multiple criteria
|
||||
// Set "selected" to true for the current criteria
|
||||
TrialCriteriaTypes.update(formData.trialCriteria, {
|
||||
$set: { selected: true }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.helpers({
|
||||
getDescriptionTemplate(_id) {
|
||||
return OHIF.lesiontracker.TrialCriteriaTypes.findOne(_id).descriptionTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.events({
|
||||
'change .js-trial'(event, instance) {
|
||||
const form = instance.$('form').first().data('component');
|
||||
if (!form) return;
|
||||
instance.selectedTrial.set(form.value().trialCriteria);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user