feat(measurement-table) Display support for measurements from DICOM Structured Reports (#225)
This commit is contained in:
parent
e9832f62f7
commit
d94f0eec16
@ -57,19 +57,19 @@
|
||||
|
||||
.sidebar-right
|
||||
flex: 1
|
||||
margin-right: - $lesionsSidebarMenuWidth
|
||||
max-width: $lesionsSidebarMenuWidth
|
||||
margin-right: - $rightSidebarMenuWidth
|
||||
max-width: $rightSidebarMenuWidth
|
||||
order: 3
|
||||
position: relative
|
||||
visibility: hidden
|
||||
|
||||
&[data-timepoints="3"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 135.5px)
|
||||
max-width: $lesionsSidebarMenuWidth + 135.5px
|
||||
margin-right: - ($rightSidebarMenuWidth + 135.5px)
|
||||
max-width: $rightSidebarMenuWidth + 135.5px
|
||||
|
||||
&[data-timepoints="4"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 270px)
|
||||
max-width: $lesionsSidebarMenuWidth + 270px
|
||||
margin-right: - ($rightSidebarMenuWidth + 270px)
|
||||
max-width: $rightSidebarMenuWidth + 270px
|
||||
|
||||
&.sidebar-open
|
||||
margin-right: 0
|
||||
|
||||
@ -46,6 +46,7 @@ ohif:hanging-protocols
|
||||
ohif:metadata
|
||||
ohif:user
|
||||
ohif:user-keycloak
|
||||
ohif:measurement-table
|
||||
|
||||
fortawesome:fontawesome
|
||||
momentjs:moment@2.15.1
|
||||
|
||||
@ -101,8 +101,11 @@ ohif:hanging-protocols@0.0.1
|
||||
ohif:header@0.0.1
|
||||
ohif:hotkeys@0.0.1
|
||||
ohif:log@0.0.1
|
||||
ohif:measurement-table@0.0.1
|
||||
ohif:measurements@0.0.1
|
||||
ohif:metadata@0.0.1
|
||||
ohif:polyfill@0.0.1
|
||||
ohif:select-tree@0.0.1
|
||||
ohif:servers@0.0.1
|
||||
ohif:studies@0.0.1
|
||||
ohif:study-list@0.0.1
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
{{> viewerMain (clone this)}}
|
||||
</div>
|
||||
<div class="sidebarMenu sidebar-right {{#if rightSidebarOpen}}sidebar-open{{/if}}">
|
||||
{{> protocolEditor (clone this)}}
|
||||
{{> measurementLightTable (clone this)}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
$lesionsSidebarMenuWidth = 450px
|
||||
|
||||
.viewerSection
|
||||
display: flex
|
||||
flex: 1
|
||||
@ -47,18 +45,18 @@ $lesionsSidebarMenuWidth = 450px
|
||||
|
||||
.sidebar-right
|
||||
flex: 1
|
||||
margin-right: - $lesionsSidebarMenuWidth
|
||||
max-width: $lesionsSidebarMenuWidth
|
||||
margin-right: - $rightSidebarMenuWidth
|
||||
max-width: $rightSidebarMenuWidth
|
||||
order: 3
|
||||
position: relative
|
||||
|
||||
&[data-timepoints="3"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 135.5px)
|
||||
max-width: $lesionsSidebarMenuWidth + 135.5px
|
||||
margin-right: - ($rightSidebarMenuWidth + 135.5px)
|
||||
max-width: $rightSidebarMenuWidth + 135.5px
|
||||
|
||||
&[data-timepoints="4"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 270px)
|
||||
max-width: $lesionsSidebarMenuWidth + 270px
|
||||
margin-right: - ($rightSidebarMenuWidth + 270px)
|
||||
max-width: $rightSidebarMenuWidth + 270px
|
||||
|
||||
&.sidebar-open
|
||||
margin-right: 0
|
||||
|
||||
@ -8,9 +8,6 @@
|
||||
<div class="pull-right m-t-1 rm-x-1">
|
||||
{{>roundedButtonGroup rightSidebarToggleButtonData}}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{{>toolbarSectionTools toolbarButtons=hangingProtocolButtons}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -47,14 +47,29 @@ Template.toolbarSection.helpers({
|
||||
|
||||
rightSidebarToggleButtonData() {
|
||||
const instance = Template.instance();
|
||||
|
||||
// TODO: Figured out a way to handle both right panel contents
|
||||
// return {
|
||||
// toggleable: true,
|
||||
// key: 'rightSidebar',
|
||||
// value: instance.data.state,
|
||||
// options: [{
|
||||
// value: 'hangingprotocols',
|
||||
// iconClasses: 'fa fa-cog',
|
||||
// bottomLabel: 'Hanging'
|
||||
// }]
|
||||
// };
|
||||
|
||||
return {
|
||||
toggleable: true,
|
||||
key: 'rightSidebar',
|
||||
value: instance.data.state,
|
||||
options: [{
|
||||
value: 'hangingprotocols',
|
||||
iconClasses: 'fa fa-cog',
|
||||
bottomLabel: 'Hanging'
|
||||
value: 'measurements',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-measurements-lesions',
|
||||
svgWidth: 18,
|
||||
svgHeight: 10,
|
||||
bottomLabel: 'Measurements'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
@ -4,6 +4,7 @@ import { Template } from 'meteor/templating';
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { MeasurementTable, measurementEvents } from 'meteor/ohif:measurement-table';
|
||||
|
||||
import 'meteor/ohif:cornerstone';
|
||||
import 'meteor/ohif:viewerbase';
|
||||
@ -64,8 +65,6 @@ Meteor.startup(() => {
|
||||
invert: viewportUtils.invert
|
||||
};
|
||||
|
||||
OHIF.viewer.stackImagePositionOffsetSynchronizer = new OHIF.viewerbase.StackImagePositionOffsetSynchronizer();
|
||||
|
||||
// Create the synchronizer used to update reference lines
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('cornerstonenewimage', cornerstoneTools.updateImageSynchronizer);
|
||||
|
||||
@ -74,6 +73,9 @@ Meteor.startup(() => {
|
||||
// Metadata configuration
|
||||
const metadataProvider = OHIF.viewer.metadataProvider;
|
||||
cornerstone.metaData.addProvider(metadataProvider.provider.bind(metadataProvider));
|
||||
|
||||
// Instanciate viewer plugins
|
||||
OHIF.viewer.measurementTable = new MeasurementTable();
|
||||
});
|
||||
|
||||
Template.viewer.onCreated(() => {
|
||||
@ -135,10 +137,15 @@ Template.viewer.onCreated(() => {
|
||||
// Updates WADO-RS metaDataManager
|
||||
OHIF.viewerbase.updateMetaDataManager(study);
|
||||
});
|
||||
|
||||
// Call Viewer plugins onCreated functions
|
||||
if(typeof OHIF.viewer.measurementTable.onCreated === 'function') {
|
||||
OHIF.viewer.measurementTable.onCreated(instance);
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
|
||||
const instance = Template.instance();
|
||||
this.autorun(function() {
|
||||
// To make sure ohif viewerMain is rendered before initializing Hanging Protocols
|
||||
const isOHIFViewerMainRendered = Session.get('OHIFViewerMainRendered');
|
||||
@ -151,20 +158,23 @@ Template.viewer.onRendered(function() {
|
||||
}
|
||||
});
|
||||
|
||||
// Call Viewer plugins onRendered functions
|
||||
if(typeof OHIF.viewer.measurementTable.onRendered === 'function') {
|
||||
OHIF.viewer.measurementTable.onRendered(instance);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Template.viewer.events({
|
||||
'click .js-toggle-studies'() {
|
||||
const instance = Template.instance();
|
||||
const current = instance.state.get('leftSidebar');
|
||||
instance.state.set('leftSidebar', !current);
|
||||
Template.viewer.events( Object.assign({
|
||||
// Viewer Events
|
||||
},
|
||||
measurementEvents
|
||||
));
|
||||
|
||||
'click .js-toggle-protocol-editor'() {
|
||||
const instance = Template.instance();
|
||||
const current = instance.state.get('rightSidebar');
|
||||
instance.data.state.set('rightSidebar', !current);
|
||||
},
|
||||
Template.viewer.onDestroyed(function() {
|
||||
if(typeof OHIF.viewer.measurementTable.onDestroyed === 'function') {
|
||||
OHIF.viewer.measurementTable.onDestroyed();
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.helpers({
|
||||
|
||||
5
OHIFViewer/package-lock.json
generated
5
OHIFViewer/package-lock.json
generated
@ -298,11 +298,6 @@
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
|
||||
@ -118,7 +118,7 @@ export class MetadataProvider {
|
||||
value = image.instance[attrName];
|
||||
}
|
||||
|
||||
return value == null ? defaultValue : value;
|
||||
return value === null ? defaultValue : value;
|
||||
}
|
||||
|
||||
getFromDataSet(dataSet, type, tag) {
|
||||
|
||||
@ -4,6 +4,7 @@ import * as cornerstoneMath from 'cornerstone-math/dist/cornerstoneMath.js';
|
||||
import * as cornerstoneTools from 'cornerstone-tools/dist/cornerstoneTools.js';
|
||||
import * as cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader/dist/cornerstoneWADOImageLoader.js';
|
||||
import * as dicomParser from 'dicom-parser'; // Importing from dist breaks instance reference of dicomParser.DataSet class
|
||||
import * as dcmjs from 'dcmjs/build/dcmjs';
|
||||
|
||||
cornerstoneTools.external.Hammer = Hammer;
|
||||
cornerstoneTools.external.cornerstone = cornerstone;
|
||||
@ -18,5 +19,6 @@ export {
|
||||
cornerstoneTools,
|
||||
cornerstoneMath,
|
||||
cornerstoneWADOImageLoader,
|
||||
dicomParser
|
||||
dicomParser,
|
||||
dcmjs
|
||||
};
|
||||
|
||||
@ -10,7 +10,8 @@ Npm.depends({
|
||||
'cornerstone-tools': '2.3.3',
|
||||
'cornerstone-math': '0.1.6',
|
||||
'dicom-parser': '1.8.0',
|
||||
'cornerstone-wado-image-loader': '2.1.4'
|
||||
'cornerstone-wado-image-loader': '2.1.4',
|
||||
'dcmjs': '0.1.3'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
@ -29,4 +30,5 @@ Package.onUse(function(api) {
|
||||
api.export('cornerstoneTools', 'client');
|
||||
api.export('cornerstoneWADOImageLoader', 'client');
|
||||
api.export('dicomParser', 'client');
|
||||
api.export('dcmjs', 'client');
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ $topBarExpandedHeight = 160px
|
||||
$toolbarHeight = 78px
|
||||
$toolbarDrawerHeight = 62px
|
||||
$studiesSidebarMenuWidth = 307px
|
||||
$lesionsSidebarMenuWidth = 323px
|
||||
$rightSidebarMenuWidth = 323px
|
||||
$studyListPadding = 8%
|
||||
$studyListPaddingMediumScreen = 10px
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ Template[defaultTemplate].onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const { studyInstanceUid, seriesInstanceUid } = instance.data;
|
||||
|
||||
Tracker.autorun(computation => {
|
||||
instance.autorun(computation => {
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
if (computation.firstRun) return;
|
||||
computation.stop();
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
import { measurementTools } from './measurementTools';
|
||||
import { retrieveMeasurements, storeMeasurements, retrieveTimepoints, storeTimepoints, removeTimepoint, updateTimepoint, disassociateStudy } from './dataExchange';
|
||||
|
||||
export const configureApis = () => {
|
||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||
measurementTools,
|
||||
dataExchange: {
|
||||
retrieve: retrieveMeasurements,
|
||||
store: storeMeasurements
|
||||
},
|
||||
dataValidation: {
|
||||
validation: () => {}
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.measurements.TimepointApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: retrieveTimepoints,
|
||||
store: storeTimepoints,
|
||||
remove: removeTimepoint,
|
||||
update: updateTimepoint,
|
||||
disassociate: disassociateStudy
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { handleSR, getLatestSRSeries } from "../utils/handleSR";
|
||||
|
||||
export const retrieveMeasurements = (patientId, timepointIds) => {
|
||||
OHIF.log.info('retrieveMeasurements');
|
||||
|
||||
const latestSeries = getLatestSRSeries();
|
||||
|
||||
if(!latestSeries) return Promise.resolve({});
|
||||
|
||||
return handleSR(latestSeries).then((value) => {
|
||||
return {
|
||||
length: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const storeMeasurements = (measurementData, timepointIds) => {
|
||||
OHIF.log.info('storeMeasurements');
|
||||
|
||||
// Here is where we should do any required data transformation and API calls
|
||||
|
||||
// TODO: Write SR, STOW back to PACS
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const retrieveTimepoints = filter => {
|
||||
const studyInstanceUids = OHIF.viewer.StudyMetadataList.all().map(study => study.getStudyInstanceUID());
|
||||
OHIF.log.info('retrieveTimepoints');
|
||||
|
||||
return Promise.resolve([{
|
||||
timepointType: 'baseline',
|
||||
timepointId: 'TimepointId',
|
||||
studyInstanceUids,
|
||||
patientId: filter.patientId,
|
||||
earliestDate: new Date(),
|
||||
latestDate: new Date(),
|
||||
isLocked: false
|
||||
}]);
|
||||
};
|
||||
|
||||
export const storeTimepoints = (timepointData) => {
|
||||
OHIF.log.info('storeTimepoints');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const updateTimepoint = (timepointData, query) => {
|
||||
OHIF.log.info('updateTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const removeTimepoint = timepointId => {
|
||||
OHIF.log.info('removeTimepoint');
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
export const disassociateStudy = (timepointIds, studyInstanceUid) => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { ToolGroupBaseSchema } from '../schema/toolGroupSchema';
|
||||
import { length } from '../schema/length';
|
||||
import { ellipticalRoi } from '../schema/ellipticalRoi';
|
||||
import { rectangleRoi } from '../schema/rectangleRoi';
|
||||
|
||||
const trackedTools = [
|
||||
length,
|
||||
ellipticalRoi,
|
||||
rectangleRoi
|
||||
];
|
||||
|
||||
export const measurementTools = [{
|
||||
id: 'allTools',
|
||||
name: 'Measurements',
|
||||
childTools: trackedTools,
|
||||
schema: ToolGroupBaseSchema
|
||||
}];
|
||||
125
Packages/ohif-measurement-table/client/index.js
Normal file
125
Packages/ohif-measurement-table/client/index.js
Normal file
@ -0,0 +1,125 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Session } from 'meteor/session';
|
||||
import { configureApis } from './configuration/configuration'
|
||||
import { $ } from 'meteor/jquery';
|
||||
|
||||
class MeasurementTable {
|
||||
constructor() {
|
||||
configureApis();
|
||||
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
|
||||
this.firstMeasurementActivated = false;
|
||||
}
|
||||
|
||||
async onCreated(instance) {
|
||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
||||
|
||||
OHIF.viewer.data.currentTimepointId = 'TimepointId';
|
||||
|
||||
const timepointApi = new TimepointApi(OHIF.viewer.data.currentTimepointId);
|
||||
const measurementApi = new MeasurementApi(timepointApi);
|
||||
const apis = {
|
||||
timepointApi,
|
||||
measurementApi
|
||||
};
|
||||
|
||||
Object.assign(OHIF.viewer, apis);
|
||||
Object.assign(instance.data, apis);
|
||||
|
||||
const patientId = instance.data.studies[0].patientId;
|
||||
|
||||
await timepointApi.retrieveTimepoints({ patientId });
|
||||
Session.set('TimepointsReady', true);
|
||||
|
||||
await measurementApi.retrieveMeasurements(patientId, [OHIF.viewer.data.currentTimepointId]);
|
||||
Session.set('MeasurementsReady', true);
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
instance.autorun(() => {
|
||||
if (!Session.get('TimepointsReady') ||
|
||||
!Session.get('MeasurementsReady') ||
|
||||
!Session.get('ViewerReady') ||
|
||||
this.firstMeasurementActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.jumpToFirstMeasurement();
|
||||
|
||||
});
|
||||
|
||||
instance.measurementModifiedHandler = _.throttle((event, instance) => {
|
||||
OHIF.measurements.MeasurementHandlers.onModified(event, instance);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
onDestroyed() {
|
||||
Session.set('TimepointsReady', false);
|
||||
Session.set('MeasurementsReady', false);
|
||||
}
|
||||
|
||||
jumpToFirstMeasurement() {
|
||||
// Find and activate the first measurement by Lesion Number
|
||||
// NOTE: This is inefficient, we should be using a hanging protocol
|
||||
// to hang the first measurement's imageId immediately, rather
|
||||
// than changing images after initial loading...
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const tools = config.measurementTools[0].childTools;
|
||||
const firstTool = tools[Object.keys(tools)[0]];
|
||||
const measurementTypeId = firstTool.id;
|
||||
|
||||
const collection = OHIF.viewer.measurementApi.tools[measurementTypeId];
|
||||
const sorting = {
|
||||
sort: {
|
||||
measurementNumber: -1
|
||||
}
|
||||
};
|
||||
|
||||
const data = collection.find({}, sorting).fetch();
|
||||
|
||||
// TODO: Clean this up, it's probably an inefficient way to get what we need
|
||||
const groupObject = _.groupBy(data, m => m.measurementNumber);
|
||||
|
||||
// Reformat the data
|
||||
const rows = Object.keys(groupObject).map(key => ({
|
||||
measurementTypeId: measurementTypeId,
|
||||
measurementNumber: key,
|
||||
entries: groupObject[key]
|
||||
}));
|
||||
|
||||
const rowItem = rows[0];
|
||||
const timepoints = [
|
||||
OHIF.viewer.timepointApi.current()
|
||||
];
|
||||
|
||||
if (rowItem) {
|
||||
OHIF.measurements.jumpToRowItem(rowItem, timepoints);
|
||||
}
|
||||
|
||||
this.firstMeasurementActivated = true;
|
||||
}
|
||||
};
|
||||
|
||||
const measurementEvents = {
|
||||
'cornerstonetoolsmeasurementadded .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onAdded(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonetoolsmeasurementmodified .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
instance.measurementModifiedHandler(originalEvent, instance);
|
||||
},
|
||||
|
||||
'cornerstonemeasurementremoved .imageViewerViewport'(event, instance) {
|
||||
const originalEvent = event.originalEvent;
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved(originalEvent, instance);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
MeasurementTable,
|
||||
measurementEvents
|
||||
}
|
||||
104
Packages/ohif-measurement-table/client/schema/ellipticalRoi.js
Normal file
104
Packages/ohif-measurement-table/client/schema/ellipticalRoi.js
Normal file
@ -0,0 +1,104 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const EllipticalRoiHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
},
|
||||
});
|
||||
|
||||
const MeanStdDevSchema = new SimpleSchema({
|
||||
count: {
|
||||
type: Number,
|
||||
label: 'count',
|
||||
decimal: true
|
||||
},
|
||||
mean: {
|
||||
type: Number,
|
||||
label: 'mean',
|
||||
decimal: true
|
||||
},
|
||||
stdDev: {
|
||||
type: Number,
|
||||
label: 'stdDev',
|
||||
decimal: true
|
||||
},
|
||||
variance: {
|
||||
type: Number,
|
||||
label: 'variance',
|
||||
decimal: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const EllipticalRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: EllipticalRoiHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'ellipticalRoi'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
area: {
|
||||
type: Number,
|
||||
label: 'Ellipse Area value',
|
||||
decimal: true,
|
||||
optional: true
|
||||
},
|
||||
meanStdDev: {
|
||||
type: MeanStdDevSchema,
|
||||
label: 'MeanStd Values',
|
||||
optional: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
if (data.meanStdDev && data.meanStdDev.mean) {
|
||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
// let meanValue = data.meanStdDev && data.meanStdDev.mean || 0;
|
||||
// return numberWithCommas(meanValue).toFixed(2) + ' HU';
|
||||
//return data.meanStdDev.mean.toFixed(2);
|
||||
};
|
||||
|
||||
export const ellipticalRoi = {
|
||||
id: 'ellipticalRoi',
|
||||
name: 'Ellipse',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'ellipticalRoi',
|
||||
schema: EllipticalRoiSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
72
Packages/ohif-measurement-table/client/schema/length.js
Normal file
72
Packages/ohif-measurement-table/client/schema/length.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const LengthHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
}
|
||||
});
|
||||
|
||||
const LengthSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: LengthHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'length'
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
label: 'Length',
|
||||
optional: true,
|
||||
decimal: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let lengthValue = '';
|
||||
if (data.length) {
|
||||
lengthValue = data.length.toFixed(2) + ' mm';
|
||||
}
|
||||
return lengthValue;
|
||||
};
|
||||
|
||||
export const length = {
|
||||
id: 'length',
|
||||
name: 'Length',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'length',
|
||||
schema: LengthSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
101
Packages/ohif-measurement-table/client/schema/rectangleRoi.js
Normal file
101
Packages/ohif-measurement-table/client/schema/rectangleRoi.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { MeasurementSchemaTypes } from 'meteor/ohif:measurements/both/schema/measurements';
|
||||
|
||||
const CornerstoneHandleSchema = MeasurementSchemaTypes.CornerstoneHandleSchema;
|
||||
|
||||
const RectangleRoiHandlesSchema = new SimpleSchema({
|
||||
start: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Start'
|
||||
},
|
||||
end: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'End'
|
||||
},
|
||||
textBox: {
|
||||
type: CornerstoneHandleSchema,
|
||||
label: 'Text Box'
|
||||
},
|
||||
});
|
||||
|
||||
const MeanStdDevSchema = new SimpleSchema({
|
||||
count: {
|
||||
type: Number,
|
||||
label: 'count',
|
||||
decimal: true
|
||||
},
|
||||
mean: {
|
||||
type: Number,
|
||||
label: 'mean',
|
||||
decimal: true
|
||||
},
|
||||
stdDev: {
|
||||
type: Number,
|
||||
label: 'stdDev',
|
||||
decimal: true
|
||||
},
|
||||
variance: {
|
||||
type: Number,
|
||||
label: 'variance',
|
||||
decimal: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const RectangleRoiSchema = new SimpleSchema([MeasurementSchemaTypes.CornerstoneToolMeasurement, {
|
||||
handles: {
|
||||
type: RectangleRoiHandlesSchema,
|
||||
label: 'Handles'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
},
|
||||
toolType: {
|
||||
type: String,
|
||||
label: 'Measurement Tool Type',
|
||||
defaultValue: 'rectangleRoi'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location',
|
||||
optional: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
label: 'Description',
|
||||
optional: true
|
||||
},
|
||||
area: {
|
||||
type: Number,
|
||||
label: 'Rectangle Area value',
|
||||
decimal: true,
|
||||
optional: true
|
||||
},
|
||||
meanStdDev: {
|
||||
type: MeanStdDevSchema,
|
||||
label: 'MeanStd Values',
|
||||
optional: true
|
||||
}
|
||||
}]);
|
||||
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
if (data.meanStdDev && data.meanStdDev.mean) {
|
||||
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
};
|
||||
|
||||
export const rectangleRoi = {
|
||||
id: 'rectangleRoi',
|
||||
name: 'Rectangle',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'rectangleRoi',
|
||||
schema: RectangleRoiSchema,
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
export const ToolGroupBaseSchema = new SimpleSchema({
|
||||
toolId: {
|
||||
type: String,
|
||||
label: 'Tool ID',
|
||||
optional: true
|
||||
},
|
||||
toolItemId: {
|
||||
type: String,
|
||||
label: 'Tool Item ID',
|
||||
optional: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
studyInstanceUid: {
|
||||
type: String,
|
||||
label: 'Study Instance UID'
|
||||
},
|
||||
timepointId: {
|
||||
type: String,
|
||||
label: 'Timepoint ID'
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
label: 'Measurement Number'
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,95 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
function getInstanceMetadata (displaySets, sopInstanceUid) {
|
||||
let instance;
|
||||
|
||||
// Use Array.some so that this loop stops when the internal loop
|
||||
// has found the correct instance
|
||||
displaySets.some(displaySet => {
|
||||
// Search the display set to find the instance metadata for
|
||||
return displaySet.images.find(instanceMetadata => {
|
||||
if (instanceMetadata._sopInstanceUID === sopInstanceUid) {
|
||||
instance = instanceMetadata;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
export default function getLengthMeasurementData(lengthMeasurementContent, displaySets) {
|
||||
let lengthStates = [];
|
||||
|
||||
lengthMeasurementContent.forEach(groupItemContent => {
|
||||
const lengthContent = groupItemContent.ContentSequence;
|
||||
const reference = lengthContent.ContentSequence.ReferencedSOPSequence;
|
||||
const lengthState = {};
|
||||
|
||||
lengthState.measuredValue = groupItemContent.MeasuredValueSequence.NumericValue;
|
||||
lengthState.handles = {start: {}, end: {}};
|
||||
[lengthState.handles.start.x,
|
||||
lengthState.handles.start.y,
|
||||
lengthState.handles.end.x,
|
||||
lengthState.handles.end.y] = lengthContent.GraphicData;
|
||||
|
||||
// TODO: Save textbox position in GraphicData?
|
||||
lengthState.handles.textBox = {
|
||||
hasMoved: false,
|
||||
movesIndependently: false,
|
||||
drawnIndependently: true,
|
||||
allowedOutsideImage: true,
|
||||
hasBoundingBox: true
|
||||
}
|
||||
|
||||
lengthState.ReferencedInstanceUID = reference.ReferencedSOPInstanceUID;
|
||||
if (reference.ReferencedFrameNumber && reference.ReferencedFrameNumber !== 'NaN') {
|
||||
lengthState.ReferencedFrameNumber = reference.ReferencedFrameNumber;
|
||||
} else {
|
||||
lengthState.ReferencedFrameNumber = 0;
|
||||
}
|
||||
|
||||
lengthStates.push(lengthState);
|
||||
});
|
||||
|
||||
const lengthMeasurementData = [];
|
||||
|
||||
let measurementNumber = 0;
|
||||
lengthStates.forEach(lengthState => {
|
||||
const sopInstanceUid = lengthState.ReferencedInstanceUID;
|
||||
const instanceMetadata = getInstanceMetadata(displaySets, sopInstanceUid);
|
||||
const imageId = OHIF.viewerbase.getImageId(instanceMetadata);
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyInstanceUid = cornerstone.metaData.get('study', imageId).studyInstanceUid;
|
||||
const seriesInstanceUid = cornerstone.metaData.get('series', imageId).seriesInstanceUid;
|
||||
const patientId = instanceMetadata._study.patientId;
|
||||
const frameIndex = lengthState.ReferencedFrameNumber;
|
||||
const imagePath = [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
|
||||
const measurement = {
|
||||
handles: lengthState.handles,
|
||||
length: lengthState.measuredValue,
|
||||
imageId,
|
||||
imagePath,
|
||||
sopInstanceUid,
|
||||
seriesInstanceUid,
|
||||
studyInstanceUid,
|
||||
patientId,
|
||||
frameIndex,
|
||||
measurementNumber: ++measurementNumber,
|
||||
userId: 'UserID',
|
||||
timepointId: OHIF.viewer.data.currentTimepointId,
|
||||
toolType: 'length',
|
||||
_id: imageId + measurementNumber,
|
||||
};
|
||||
|
||||
lengthMeasurementData.push(measurement);
|
||||
});
|
||||
|
||||
|
||||
return lengthMeasurementData;
|
||||
};
|
||||
100
Packages/ohif-measurement-table/client/utils/handleSR.js
Normal file
100
Packages/ohif-measurement-table/client/utils/handleSR.js
Normal file
@ -0,0 +1,100 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { dcmjs } from 'meteor/ohif:cornerstone';
|
||||
import getLengthMeasurementData from './getLengthMeasurementData';
|
||||
|
||||
const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22'];
|
||||
|
||||
const toArray = function(x) {
|
||||
return (x.constructor.name === "Array" ? x : [x]);
|
||||
};
|
||||
|
||||
const codeMeaningEquals = (codeMeaningName) => {
|
||||
return (contentItem) => {
|
||||
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
|
||||
};
|
||||
};
|
||||
|
||||
const imagingMeasurementsToMeasurementData = (dataset, displaySets) => {
|
||||
// Identify the Imaging Measurements
|
||||
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(codeMeaningEquals("Imaging Measurements"));
|
||||
|
||||
// Retrieve the Measurements themselves
|
||||
const measurementGroupContent = toArray(imagingMeasurementContent.ContentSequence).find(codeMeaningEquals("Measurement Group"));
|
||||
|
||||
// For now, bail out if the dataset is not a TID1500 SR with length measurements
|
||||
// TODO: generalize to the various kinds of report
|
||||
// TODO: generalize to the kinds of measurements the Viewer supports
|
||||
if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") {
|
||||
OHIF.log.warn("This package can currently only interpret DICOM SR TID 1500");
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Filter to find Length measurements in the Structured Report
|
||||
const lengthMeasurementContent = toArray(measurementGroupContent.ContentSequence).filter(codeMeaningEquals("Length"));
|
||||
|
||||
// Retrieve Length Measurement Data
|
||||
return getLengthMeasurementData(lengthMeasurementContent, displaySets);
|
||||
};
|
||||
|
||||
const retrieveDataFromSR = (Part10SRArrayBuffer) => {
|
||||
const allStudies = OHIF.viewer.Studies.all();
|
||||
let allDisplaySets = [];
|
||||
|
||||
allStudies.forEach(study => {
|
||||
allDisplaySets = allDisplaySets.concat(study.displaySets);
|
||||
});
|
||||
|
||||
// get the dicom data as an Object
|
||||
let dicomData = dcmjs.data.DicomMessage.readFile(Part10SRArrayBuffer);
|
||||
let dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
|
||||
// Convert the SR into the kind of object the Measurements package is expecting
|
||||
return imagingMeasurementsToMeasurementData(dataset, allDisplaySets);
|
||||
};
|
||||
|
||||
export const getLatestSRSeries = () => {
|
||||
const allStudies = OHIF.viewer.StudyMetadataList.all();
|
||||
let latestSeries;
|
||||
|
||||
allStudies.forEach(study => {
|
||||
study.getSeries().forEach(series => {
|
||||
const firstInstance = series.getFirstInstance();
|
||||
const sopClassUid = firstInstance._instance.sopClassUid;
|
||||
|
||||
if (supportedSopClassUIDs.includes(sopClassUid)) {
|
||||
if(!latestSeries) {
|
||||
latestSeries = series;
|
||||
} else if (series._data.seriesDate > latestSeries._data.seriesDate ||
|
||||
(series._data.seriesDate === latestSeries._data.seriesDate && series._data.seriesTime > latestSeries._data.seriesTime)) {
|
||||
latestSeries = series;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return latestSeries;
|
||||
};
|
||||
|
||||
export const handleSR = (series) => {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.responseType = 'arraybuffer';
|
||||
request.open('GET', instance.getDataProperty('wadouri'));
|
||||
|
||||
request.onload = function (progressEvent) {
|
||||
const data = retrieveDataFromSR(progressEvent.currentTarget.response);
|
||||
|
||||
resolve(data);
|
||||
};
|
||||
|
||||
request.onerror = function(error) {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.send();
|
||||
});
|
||||
};
|
||||
20
Packages/ohif-measurement-table/package.js
Normal file
20
Packages/ohif-measurement-table/package.js
Normal file
@ -0,0 +1,20 @@
|
||||
Package.describe({
|
||||
name: 'ohif:measurement-table',
|
||||
summary: 'OHIF Measurement table',
|
||||
version: '0.0.1'
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.6');
|
||||
|
||||
api.use('ecmascript');
|
||||
|
||||
// Our custom packages
|
||||
api.use('ohif:cornerstone');
|
||||
api.use('ohif:core');
|
||||
api.use('ohif:cornerstone-settings');
|
||||
api.use('ohif:viewerbase');
|
||||
api.use('ohif:measurements');
|
||||
|
||||
api.mainModule('client/index.js', 'client');
|
||||
});
|
||||
@ -10,11 +10,13 @@ const Measurement = new SimpleSchema({
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
label: 'User ID'
|
||||
label: 'User ID',
|
||||
optional: true
|
||||
},
|
||||
patientId: {
|
||||
type: String,
|
||||
label: 'Patient ID'
|
||||
label: 'Patient ID',
|
||||
optional: true
|
||||
},
|
||||
measurementNumber: {
|
||||
type: Number,
|
||||
|
||||
@ -4,6 +4,7 @@ export const schema = new SimpleSchema({
|
||||
patientId: {
|
||||
type: String,
|
||||
label: 'Patient ID',
|
||||
optional: true
|
||||
},
|
||||
timepointId: {
|
||||
type: String,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ export default function({ instance, eventData, tool, toolGroupId, toolGroup }) {
|
||||
// Stop here if the tool data shall not be persisted (e.g. temp tools)
|
||||
if (!Collection) return;
|
||||
|
||||
const measurementTypeId = measurementApi.toolsGroupsMap[measurementData.toolType];
|
||||
const measurementTypeId = measurementApi.toolsGroupsMap[eventData.toolType];
|
||||
const measurement = Collection.findOne(measurementData._id);
|
||||
|
||||
// Stop here if the measurement is already gone or never existed
|
||||
|
||||
154
Packages/ohif-measurements/client/lib/getLabelTerminologyList.js
Normal file
154
Packages/ohif-measurements/client/lib/getLabelTerminologyList.js
Normal file
@ -0,0 +1,154 @@
|
||||
const segmentedTerminologyList = [{
|
||||
label:'Abdomen/Chest Wall',
|
||||
value:'Abdomen/Chest Wall',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Adrenal',
|
||||
value:'Adrenal',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Bladder',
|
||||
value:'Bladder',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Bone',
|
||||
value:'Bone',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Brain',
|
||||
value:'Brain',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Breast',
|
||||
value:'Breast',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Colon',
|
||||
value:'Colon',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Esophagus',
|
||||
value:'Esophagus',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Extremities',
|
||||
value:'Extremities',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Gallbladder',
|
||||
value:'Gallbladder',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Kidney',
|
||||
value:'Kidney',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Liver',
|
||||
value:'Liver',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Lung',
|
||||
value:'Lung',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Lymph Node',
|
||||
value:'Lymph Node',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Mediastinum/Hilum',
|
||||
value:'Mediastinum/Hilum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Muscle',
|
||||
value:'Muscle',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Neck',
|
||||
value:'Neck',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Other Soft Tissue',
|
||||
value:'Other Soft Tissue',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Ovary',
|
||||
value:'Ovary',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Pancreas',
|
||||
value:'Pancreas',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Pelvis',
|
||||
value:'Pelvis',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Peritoneum/Omentum',
|
||||
value:'Peritoneum/Omentum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Prostate',
|
||||
value:'Prostate',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Retroperitoneum',
|
||||
value:'Retroperitoneum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Small Bowel',
|
||||
value:'Small Bowel',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Spleen',
|
||||
value:'Spleen',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Stomach',
|
||||
value:'Stomach',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Subcutaneous',
|
||||
value:'Subcutaneous',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
}
|
||||
];
|
||||
|
||||
const segmentedTerminologyCommonList = [{
|
||||
label:'Abdomen/Chest Wall',
|
||||
value:'Abdomen/Chest Wall',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Liver',
|
||||
value:'Liver',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Lung',
|
||||
value:'Lung',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Lymph Node',
|
||||
value:'Lymph Node',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Mediastinum/Hilum',
|
||||
value:'Mediastinum/Hilum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Pelvis',
|
||||
value:'Pelvis',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Peritoneum/Omentum',
|
||||
value:'Peritoneum/Omentum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
},{
|
||||
label:'Retroperitoneum',
|
||||
value:'Retroperitoneum',
|
||||
segmentedPropCategory: 'T-D0080'
|
||||
}
|
||||
];
|
||||
|
||||
export {
|
||||
segmentedTerminologyList,
|
||||
segmentedTerminologyCommonList
|
||||
}
|
||||
@ -10,6 +10,15 @@ OHIF.measurements.getLocation = collection => {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: change this to a const after refactoring newMeasurements code on measurementTableView.js
|
||||
OHIF.measurements.getDescription = collection => {
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
if (collection[i].description) {
|
||||
return collection[i].description;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Group all measurements by its tool group and measurement number.
|
||||
*
|
||||
@ -76,6 +85,7 @@ OHIF.measurements.getMeasurementsGroupedByNumber = (measurementApi, timepointApi
|
||||
measurementTypeId: toolGroup.id,
|
||||
measurementNumber: key,
|
||||
location: OHIF.measurements.getLocation(groupObject[key]),
|
||||
description: OHIF.measurements.getDescription(groupObject[key]),
|
||||
responseStatus: false, // TODO: Get the latest timepoint and determine the response status
|
||||
entries: groupObject[key]
|
||||
}));
|
||||
|
||||
@ -19,5 +19,6 @@ import './navigateOverLesions';
|
||||
import './saveMeasurements';
|
||||
import './syncMeasurementAndToolData';
|
||||
import './toggleLabelButton';
|
||||
import './openLocationModal';
|
||||
import './triggerTimepointUnsavedChanges';
|
||||
import './updateMeasurementsDescription';
|
||||
|
||||
@ -55,6 +55,9 @@ function renderIntoViewport(measurementData, enabledElement, viewportIndex) {
|
||||
|
||||
function syncViewports(viewportsIndexes) {
|
||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||
|
||||
if(!synchronizer) { return; }
|
||||
|
||||
const linkableViewports = synchronizer.getLinkableViewports();
|
||||
if (linkableViewports.length) {
|
||||
const linkableViewportsIndexes = _.pluck(linkableViewports, 'index');
|
||||
@ -141,7 +144,10 @@ OHIF.measurements.jumpToRowItem = (rowItem, timepoints, childToolKey) => {
|
||||
const activatedViewportIndexes = [];
|
||||
|
||||
// Deactivate stack synchronizer because it will be re-activated later
|
||||
OHIF.viewer.stackImagePositionOffsetSynchronizer.deactivate();
|
||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||
if(synchronizer) {
|
||||
synchronizer.deactivate();
|
||||
}
|
||||
|
||||
const renderPromises = [];
|
||||
for (let viewportIndex = 0; viewportIndex < numViewportsToUpdate; viewportIndex++) {
|
||||
|
||||
61
Packages/ohif-measurements/client/lib/openLocationModal.js
Normal file
61
Packages/ohif-measurements/client/lib/openLocationModal.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.measurements.openLocationModal = options => {
|
||||
let { toolType } = options.measurement;
|
||||
const { tool } = OHIF.measurements.getToolConfiguration(toolType);
|
||||
|
||||
if (!tool) return;
|
||||
|
||||
toolType = (tool && tool.parentTool) || toolType;
|
||||
|
||||
const measurementId = options.measurement._id;
|
||||
let buttonView = null;
|
||||
|
||||
const removeButtonView = () => {
|
||||
Blaze.remove(buttonView);
|
||||
buttonView = null;
|
||||
};
|
||||
|
||||
if (buttonView) {
|
||||
removeButtonView();
|
||||
}
|
||||
|
||||
const measurementApi = options.measurementApi;
|
||||
const toolCollection = measurementApi.tools[toolType];
|
||||
const measurement = toolCollection.findOne(measurementId);
|
||||
|
||||
const data = {
|
||||
measurement,
|
||||
position: options.position,
|
||||
direction: options.direction,
|
||||
threeColumns: true,
|
||||
hideCommon: true,
|
||||
autoClick: options.autoClick,
|
||||
doneCallback: removeButtonView,
|
||||
updateCallback(location) {
|
||||
const groupId = measurementApi.toolsGroupsMap[toolType];
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const group = _.findWhere(config.measurementTools, { id: groupId });
|
||||
group.childTools.forEach(tool => {
|
||||
measurementApi.tools[tool.id].update({
|
||||
measurementNumber: measurement.measurementNumber,
|
||||
patientId: measurement.patientId
|
||||
}, {
|
||||
$set: {
|
||||
location
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
});
|
||||
options.measurement.location = location;
|
||||
|
||||
// Notify that viewer suffered changes
|
||||
OHIF.measurements.triggerTimepointUnsavedChanges('relabel');
|
||||
}
|
||||
};
|
||||
buttonView = Blaze.renderWithData(Template.measurementRelabel, data, document.body);
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
Npm.depends({
|
||||
ajv: '4.10.4',
|
||||
url: '0.11.0',
|
||||
jspdf: '1.3.3'
|
||||
});
|
||||
|
||||
|
||||
@ -24,6 +24,13 @@ OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => {
|
||||
OHIF.studies.loadingDict.set(studyInstanceUid, 'loading');
|
||||
}
|
||||
|
||||
const studyLoaded = OHIF.viewer.Studies.findBy({ studyInstanceUid: studyInstanceUid });
|
||||
if (studyLoaded) {
|
||||
OHIF.studies.loadingDict.set(studyInstanceUid, 'loaded');
|
||||
resolve(studyLoaded);
|
||||
return;
|
||||
}
|
||||
|
||||
return OHIF.studies.retrieveStudyMetadata(studyInstanceUid).then(study => {
|
||||
if (window.HipaaLogger && Meteor.user && Meteor.user()) {
|
||||
window.HipaaLogger.logEvent({
|
||||
@ -53,13 +60,9 @@ OHIF.studies.loadStudy = studyInstanceUid => new Promise((resolve, reject) => {
|
||||
studyMetadata.addDisplaySet(displaySet);
|
||||
});
|
||||
|
||||
// Double check to make sure this study wasn't already inserted into OHIF.viewer.Studies
|
||||
// so we don't cause duplicate entry errors
|
||||
const loaded = OHIF.viewer.Studies.findBy({ studyInstanceUid: study.studyInstanceUid });
|
||||
if (!loaded) {
|
||||
OHIF.viewer.Studies.insert(study);
|
||||
OHIF.viewer.StudyMetadataList.insert(study);
|
||||
}
|
||||
// Persist study data into OHIF.viewer
|
||||
OHIF.viewer.Studies.insert(study);
|
||||
OHIF.viewer.StudyMetadataList.insert(study);
|
||||
|
||||
// Add the study to the loading listener to allow loading progress handling
|
||||
const studyLoadingListener = OHIF.viewerbase.StudyLoadingListener.getInstance();
|
||||
|
||||
@ -61,9 +61,10 @@ export function setActiveViewport(element) {
|
||||
StudyPrefetcher.getInstance().prefetch();
|
||||
|
||||
// @TODO Add this to OHIFAfterActivateViewport handler...
|
||||
if (OHIF.viewer.stackImagePositionOffsetSynchronizer) {
|
||||
OHIF.viewer.stackImagePositionOffsetSynchronizer.update();
|
||||
}
|
||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||
if (!synchronizer) { return; }
|
||||
|
||||
synchronizer.update();
|
||||
}
|
||||
|
||||
// Set the div to focused, so keypress events are handled
|
||||
|
||||
@ -171,9 +171,7 @@ const clearTools = () => {
|
||||
const linkStackScroll = () => {
|
||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||
|
||||
if (!synchronizer) {
|
||||
return;
|
||||
}
|
||||
if (!synchronizer) { return; }
|
||||
|
||||
if (synchronizer.isActive()) {
|
||||
synchronizer.deactivate();
|
||||
@ -348,6 +346,9 @@ const isStackScrollLinkingActive = () => {
|
||||
Session.get('LayoutManagerUpdated');
|
||||
|
||||
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
|
||||
|
||||
if (!synchronizer) { return; }
|
||||
|
||||
const syncedElements = _.pluck(synchronizer.syncedViewports, 'element');
|
||||
const $renderedViewports = $('.imageViewerViewport');
|
||||
$renderedViewports.each((index, element) => {
|
||||
|
||||
@ -28,6 +28,7 @@ ohif:viewerbase
|
||||
ohif:metadata
|
||||
ohif:study-list
|
||||
ohif:dicomweb-client
|
||||
ohif:measurement-table
|
||||
|
||||
aldeed:template-extension
|
||||
aldeed:simple-schema@1.5.3
|
||||
|
||||
@ -83,11 +83,15 @@ ohif:cornerstone-settings@0.0.1
|
||||
ohif:design@0.0.1
|
||||
ohif:dicom-services@0.0.1
|
||||
ohif:dicomweb-client@0.0.1
|
||||
ohif:hanging-protocols@0.0.1
|
||||
ohif:header@0.0.1
|
||||
ohif:hotkeys@0.0.1
|
||||
ohif:log@0.0.1
|
||||
ohif:measurement-table@0.0.1
|
||||
ohif:measurements@0.0.1
|
||||
ohif:metadata@0.0.1
|
||||
ohif:polyfill@0.0.1
|
||||
ohif:select-tree@0.0.1
|
||||
ohif:servers@0.0.1
|
||||
ohif:studies@0.0.1
|
||||
ohif:study-list@0.0.1
|
||||
|
||||
@ -3,8 +3,11 @@
|
||||
<div class="sidebarMenu sidebar-left {{#if leftSidebarOpen}}sidebar-open{{/if}}">
|
||||
{{>studyBrowser (clone this)}}
|
||||
</div>
|
||||
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}}">
|
||||
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}} {{#if rightSidebarOpen}}sidebar-right-open{{/if}}">
|
||||
{{>standaloneViewerMain (clone this)}}
|
||||
</div>
|
||||
<div class="sidebarMenu sidebar-right {{#if rightSidebarOpen}}sidebar-open{{/if}}">
|
||||
{{> measurementLightTable (clone this)}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -13,5 +13,8 @@ Template.flexboxLayout.events({
|
||||
Template.flexboxLayout.helpers({
|
||||
leftSidebarOpen() {
|
||||
return Template.instance().data.state.get('leftSidebar');
|
||||
},
|
||||
rightSidebarOpen() {
|
||||
return Template.instance().data.state.get('rightSidebar');
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
$lesionsSidebarMenuWidth = 450px
|
||||
|
||||
.viewerSection
|
||||
display: flex
|
||||
flex: 1
|
||||
@ -47,18 +45,18 @@ $lesionsSidebarMenuWidth = 450px
|
||||
|
||||
.sidebar-right
|
||||
flex: 1
|
||||
margin-right: - $lesionsSidebarMenuWidth
|
||||
max-width: $lesionsSidebarMenuWidth
|
||||
margin-right: - $rightSidebarMenuWidth
|
||||
max-width: $rightSidebarMenuWidth
|
||||
order: 3
|
||||
position: relative
|
||||
|
||||
&[data-timepoints="3"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 135.5px)
|
||||
max-width: $lesionsSidebarMenuWidth + 135.5px
|
||||
margin-right: - ($rightSidebarMenuWidth + 135.5px)
|
||||
max-width: $rightSidebarMenuWidth + 135.5px
|
||||
|
||||
&[data-timepoints="4"]
|
||||
margin-right: - ($lesionsSidebarMenuWidth + 270px)
|
||||
max-width: $lesionsSidebarMenuWidth + 270px
|
||||
margin-right: - ($rightSidebarMenuWidth + 270px)
|
||||
max-width: $rightSidebarMenuWidth + 270px
|
||||
|
||||
&.sidebar-open
|
||||
margin-right: 0
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
{{>roundedButtonGroup leftSidebarToggleButtonData}}
|
||||
</div>
|
||||
{{>toolbarSectionTools toolbarButtons=toolbarButtons}}
|
||||
<div class="pull-right m-t-1 rm-x-1">
|
||||
{{>roundedButtonGroup rightSidebarToggleButtonData}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -27,6 +27,22 @@ Template.toolbarSection.helpers({
|
||||
};
|
||||
},
|
||||
|
||||
rightSidebarToggleButtonData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
toggleable: true,
|
||||
key: 'rightSidebar',
|
||||
value: instance.data.state,
|
||||
options: [{
|
||||
value: 'measurements',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-measurements-lesions',
|
||||
svgWidth: 18,
|
||||
svgHeight: 10,
|
||||
bottomLabel: 'Measurements'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
toolbarButtons() {
|
||||
const extraTools = [];
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { MeasurementTable, measurementEvents } from 'meteor/ohif:measurement-table';
|
||||
|
||||
import 'meteor/ohif:viewerbase';
|
||||
import 'meteor/ohif:metadata';
|
||||
|
||||
@ -48,6 +50,9 @@ const initHangingProtocol = () => {
|
||||
};
|
||||
|
||||
Template.viewer.onCreated(() => {
|
||||
|
||||
OHIF.viewer.measurementTable = new MeasurementTable();
|
||||
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.state = new ReactiveDict();
|
||||
@ -98,10 +103,15 @@ Template.viewer.onCreated(() => {
|
||||
OHIF.viewer.StudyMetadataList.insert(studyMetadata);
|
||||
OHIF.viewer.data.studyInstanceUids.push(study.studyInstanceUid);
|
||||
});
|
||||
|
||||
// Call Viewer plugins onCreated functions
|
||||
if(typeof OHIF.viewer.measurementTable.onCreated === 'function') {
|
||||
OHIF.viewer.measurementTable.onCreated(instance);
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.onRendered(function() {
|
||||
|
||||
const instance = Template.instance();
|
||||
this.autorun(function() {
|
||||
// To make sure ohif viewerMain is rendered before initializing Hanging Protocols
|
||||
const isOHIFViewerMainRendered = Session.get('OHIFViewerMainRendered');
|
||||
@ -114,16 +124,18 @@ Template.viewer.onRendered(function() {
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Template.viewer.events({
|
||||
'click .js-toggle-studies'() {
|
||||
const instance = Template.instance();
|
||||
const current = instance.state.get('leftSidebar');
|
||||
instance.state.set('leftSidebar', !current);
|
||||
// Call Viewer plugins onRendered functions
|
||||
if(typeof OHIF.viewer.measurementTable.onRendered === 'function') {
|
||||
OHIF.viewer.measurementTable.onRendered(instance);
|
||||
}
|
||||
});
|
||||
|
||||
Template.viewer.events( Object.assign({
|
||||
// Viewer Events
|
||||
},
|
||||
measurementEvents
|
||||
));
|
||||
|
||||
Template.viewer.helpers({
|
||||
state() {
|
||||
return Template.instance().state;
|
||||
|
||||
@ -8,6 +8,7 @@ FROM node:8.10.0-slim as builder
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
g++ \
|
||||
git \
|
||||
build-essential
|
||||
|
||||
RUN curl https://install.meteor.com/ | sh
|
||||
|
||||
@ -8,6 +8,7 @@ FROM node:8.10.0-slim as builder
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
g++ \
|
||||
git \
|
||||
build-essential
|
||||
|
||||
RUN curl https://install.meteor.com/ | sh
|
||||
|
||||
Loading…
Reference in New Issue
Block a user