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

This commit is contained in:
Gustavo André Lelis 2018-07-20 07:40:09 -03:00 committed by Erik Ziegler
parent e9832f62f7
commit d94f0eec16
64 changed files with 1797 additions and 78 deletions

View File

@ -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

View File

@ -46,6 +46,7 @@ ohif:hanging-protocols
ohif:metadata
ohif:user
ohif:user-keycloak
ohif:measurement-table
fortawesome:fontawesome
momentjs:moment@2.15.1

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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'
}]
};
},

View File

@ -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({

View File

@ -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",

View File

@ -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) {

View File

@ -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
};

View File

@ -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');
});

View File

@ -6,7 +6,7 @@ $topBarExpandedHeight = 160px
$toolbarHeight = 78px
$toolbarDrawerHeight = 62px
$studiesSidebarMenuWidth = 307px
$lesionsSidebarMenuWidth = 323px
$rightSidebarMenuWidth = 323px
$studyListPadding = 8%
$studyListPaddingMediumScreen = 10px

View File

@ -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();

View File

@ -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
}
});
};

View File

@ -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();
};

View File

@ -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
}];

View 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
}

View 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
}
}
};

View 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
}
}
};

View 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
}
}
};

View File

@ -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'
}
});

View File

@ -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;
};

View 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();
});
};

View 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');
});

View File

@ -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,

View File

@ -4,6 +4,7 @@ export const schema = new SimpleSchema({
patientId: {
type: String,
label: 'Patient ID',
optional: true
},
timepointId: {
type: String,

View File

@ -1,4 +1,5 @@
import './association';
import './caseProgress';
import './measurementTable';
import './measurementLightTable';
import './measureFlow';

View File

@ -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';

View File

@ -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>

View File

@ -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 }
});
});
});

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,6 @@
<template name="measurementLightTableHeaderRow">
<div class="measurementLightTableHeaderRow">
<div class="type">{{toolGroup.name}}</div>
<div class="numberOfMeasurements">{{measurementRows.length}}</div>
</div>
</template>

View File

@ -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

View File

@ -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>

View File

@ -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');
});
}
});

View File

@ -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')

View File

@ -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>

View File

@ -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);
});
});

View File

@ -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

View File

@ -0,0 +1,4 @@
<template name="measurementRelabel">
<div class="measurement-relabel {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}" tabindex="-1">
</div>
</template>

View File

@ -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();
}
});

View File

@ -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)
}
}

View File

@ -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

View 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
}

View File

@ -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]
}));

View File

@ -19,5 +19,6 @@ import './navigateOverLesions';
import './saveMeasurements';
import './syncMeasurementAndToolData';
import './toggleLabelButton';
import './openLocationModal';
import './triggerTimepointUnsavedChanges';
import './updateMeasurementsDescription';

View File

@ -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++) {

View 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);
};

View File

@ -1,5 +1,6 @@
Npm.depends({
ajv: '4.10.4',
url: '0.11.0',
jspdf: '1.3.3'
});

View File

@ -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();

View File

@ -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

View File

@ -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) => {

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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');
}
});

View File

@ -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

View File

@ -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>

View File

@ -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 = [];

View File

@ -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;

View File

@ -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

View File

@ -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