Skip to content

Commit

Permalink
feat: 🎸 MeasurementService (OHIF#1314)
Browse files Browse the repository at this point in the history
* feat: 🎸 MeasurementService

Work in progress...

* Remove cornerstone tools import

* Second iteration

* CR Update: Add update / added events example

* Add new props to measurements

* Update event log

* Add new measurementid to annotation

* Add context support

* Add value types

* Add area

* Add todo

* Wip measurement to annotation map

* Change points representation

* Add props to annotation mapping

* Add tests

* Extract formatter from init and add tests

* Sketch matchers

* Fix events and valuetypes imports

* Remove context support

* Rename formatter to mappings

* Sketching source and source definitions

* Adjust matching criteria in addOrUpdate

* CR Updates: Extract private functions and rename variables

* Fix broken tests

* Add more measurement service tests

* Update broken mapping tests

* Update test description

* Update getAnnotation to get mapping based on def and source

Co-authored-by: Danny Brown <[email protected]>
  • Loading branch information
igoroctaviano and dannyrb authored Feb 10, 2020
1 parent e8d56c1 commit 0c37a40
Show file tree
Hide file tree
Showing 14 changed files with 1,255 additions and 6 deletions.
1 change: 1 addition & 0 deletions extensions/cornerstone/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("../../babel.config.js");
13 changes: 13 additions & 0 deletions extensions/cornerstone/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const base = require('../../jest.config.base.js');
const pkg = require('./package');

module.exports = {
...base,
name: pkg.name,
displayName: pkg.name,
// rootDir: "../.."
// testMatch: [
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
// "<rootDir>/platform/viewer/**/*.test.js"
// ]
};
4 changes: 3 additions & 1 deletion extensions/cornerstone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"start": "yarn run dev"
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
Expand Down
118 changes: 115 additions & 3 deletions extensions/cornerstone/src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import csTools from 'cornerstone-tools';
import merge from 'lodash.merge';
import queryString from 'query-string';
import initCornerstoneTools from './initCornerstoneTools.js';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';

function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
Expand All @@ -17,7 +18,7 @@ function fallbackMetaDataProvider(type, imageId) {
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
qs.seriesUID
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;

return cornerstone.metaData.get(type, wadoRsImageId);
}
Expand All @@ -32,9 +33,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration }) {
const callInputDialog = (data, event, callback) => {
const { UIDialogService } = servicesManager.services;
const { UIDialogService, MeasurementService } = servicesManager.services;

const callInputDialog = (data, event, callback) => {
if (UIDialogService) {
let dialogId = UIDialogService.create({
centralize: true,
Expand Down Expand Up @@ -107,6 +108,9 @@ export default function init({ servicesManager, configuration }) {
tools.push(...toolsGroupedByType[toolsGroup])
);

/* Measurement Service */
_connectToolsToMeasurementService(MeasurementService);

/* Add extension tools configuration here. */
const internalToolsConfig = {
ArrowAnnotate: {
Expand Down Expand Up @@ -182,3 +186,111 @@ export default function init({ servicesManager, configuration }) {
csTools.setToolActive('ZoomTouchPinch', {});
csTools.setToolEnabled('Overlay', {});
}

const _initMeasurementService = measurementService => {
/* Initialization */
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(measurementService);
const csToolsVer4MeasurementSource = measurementService.createSource(
'CornerstoneTools',
'4'
);

/* Matching Criterias */
const matchingCriteria = {
valueType: measurementService.VALUE_TYPES.POLYLINE,
points: 2,
};

/* Mappings */
measurementService.addMapping(
csToolsVer4MeasurementSource,
'Length',
matchingCriteria,
toAnnotation,
toMeasurement
);

return csToolsVer4MeasurementSource;
};

const _connectToolsToMeasurementService = measurementService => {
const csToolsVer4MeasurementSource = _initMeasurementService(measurementService);
const {
id: sourceId,
addOrUpdate,
getAnnotation,
} = csToolsVer4MeasurementSource;

/* Measurement Service Events */
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
event => {
const {
MEASUREMENT_ADDED,
MEASUREMENT_UPDATED,
} = measurementService.EVENTS;

measurementService.subscribe(
MEASUREMENT_ADDED,
({ source, measurement }) => {
if (![sourceId].includes(source.id)) {
const annotation = getAnnotation('Length', measurement.id);

console.log(
'Measurement Service [Cornerstone]: Measurement added',
measurement
);
console.log('Mapped annotation:', annotation);
}
});

measurementService.subscribe(
MEASUREMENT_UPDATED,
({ source, measurement }) => {
if (![sourceId].includes(source.id)) {
const annotation = getAnnotation('Length', measurement.id);

console.log(
'Measurement Service [Cornerstone]: Measurement updated',
measurement
);
console.log('Mapped annotation:', annotation);
}
}
);

const addOrUpdateMeasurement = csToolsAnnotation => {
try {
const { toolName, toolType, measurementData } = csToolsAnnotation;
const csTool = toolName || measurementData.toolType || toolType;
csToolsAnnotation.id = measurementData._measurementServiceId;
const measurementServiceId = addOrUpdate(csTool, csToolsAnnotation);

if (!measurementData._measurementServiceId) {
addMeasurementServiceId(measurementServiceId, csToolsAnnotation);
}
} catch (error) {
console.warn('Failed to add or update measurement:', error);
}
};

const addMeasurementServiceId = (id, csToolsAnnotation) => {
const { measurementData } = csToolsAnnotation;
Object.assign(measurementData, { _measurementServiceId: id });
};

[
csTools.EVENTS.MEASUREMENT_ADDED,
csTools.EVENTS.MEASUREMENT_MODIFIED,
].forEach(csToolsEvtName => {
event.detail.element.addEventListener(
csToolsEvtName,
({ detail: csToolsAnnotation }) => {
console.log(`Cornerstone Element Event: ${csToolsEvtName}`);
addOrUpdateMeasurement(csToolsAnnotation);
}
);
});
}
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import cornerstone from 'cornerstone-core';

const SUPPORTED_TOOLS = ['Length', 'EllipticalRoi', 'RectangleRoi', 'ArrowAnnotate'];

const measurementServiceMappingsFactory = measurementService => {
/**
* Maps measurement service format object to cornerstone annotation object.
*
* @param {Measurement} measurement The measurement instance
* @param {string} definition The source definition
* @return {Object} Cornerstone annotation data
*/
const toAnnotation = (measurement, definition) => {
const {
id,
label,
description,
points,
unit,
sopInstanceUID,
frameOfReferenceUID,
referenceSeriesUID,
} = measurement;

return {
toolName: definition,
measurementData: {
sopInstanceUid: sopInstanceUID,
frameOfReferenceUid: frameOfReferenceUID,
seriesInstanceUid: referenceSeriesUID,
unit,
text: label,
description,
handles: _getHandlesFromPoints(points),
_measurementServiceId: id,
},
};
};

/**
* Maps cornerstone annotation event data to measurement service format.
*
* @param {Object} cornerstone Cornerstone event data
* @return {Measurement} Measurement instance
*/
const toMeasurement = csToolsAnnotation => {
const { element, measurementData } = csToolsAnnotation;
const tool =
csToolsAnnotation.toolType ||
csToolsAnnotation.toolName ||
measurementData.toolType;

const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);

if (!validToolType(tool)) {
throw new Error('Tool not supported');
}

const {
sopInstanceUid,
frameOfReferenceUid,
seriesInstanceUid,
} = _getAttributes(element);

const points = [];
points.push(measurementData.handles);

return {
id: measurementData._measurementServiceId,
sopInstanceUID: sopInstanceUid,
frameOfReferenceUID: frameOfReferenceUid,
referenceSeriesUID: seriesInstanceUid,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
area: measurementData.cachedStats && measurementData.cachedStats.area, /* TODO: Add concept names instead (descriptor) */
type: _getValueTypeFromToolType(tool),
points: _getPointsFromHandles(measurementData.handles),
};
};

const _getAttributes = element => {
const enabledElement = cornerstone.getEnabledElement(element);
const imageId = enabledElement.image.imageId;
const sopInstance = cornerstone.metaData.get('instance', imageId);
const sopInstanceUid = sopInstance.sopInstanceUid;
const frameOfReferenceUid = sopInstance.frameOfReferenceUID;
const series = cornerstone.metaData.get('series', imageId);
const seriesInstanceUid = series.seriesInstanceUid;

return { sopInstanceUid, frameOfReferenceUid, seriesInstanceUid };
};

const _getValueTypeFromToolType = toolType => {
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;

/* TODO: Relocate static value types */
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
RectangleRoi: POLYLINE,
ArrowAnnotate: POINT,
};

return TOOL_TYPE_TO_VALUE_TYPE[toolType];
};

const _getPointsFromHandles = handles => {
let points = [];
Object.keys(handles).map(handle => {
if (['start', 'end'].includes(handle)) {
let point = {};
if (handles[handle].x) point.x = handles[handle].x;
if (handles[handle].y) point.y = handles[handle].y;
points.push(point);
}
});
return points;
};

const _getHandlesFromPoints = points => {
return points
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
};

return {
toAnnotation,
toMeasurement,
};
};

export default measurementServiceMappingsFactory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';

jest.mock('cornerstone-core', () => ({
...jest.requireActual('cornerstone-core'),
getEnabledElement: () => ({
image: { imageId: 123 },
}),
metaData: {
...jest.requireActual('cornerstone-core').metaData,
get: () => ({
sopInstanceUid: "123",
frameOfReferenceUID: "123",
seriesInstanceUid: "123",
}),
},
}));

describe('measurementServiceMappings.js', () => {
let mappings;
let handles;
let points;
let csToolsAnnotation;
let measurement;
let measurementServiceMock;
let definition = 'Length';

beforeEach(() => {
measurementServiceMock = {
VALUE_TYPES: {
POLYLINE: 'value_type::polyline',
POINT: 'value_type::point',
ELLIPSE: 'value_type::ellipse',
MULTIPOINT: 'value_type::multipoint',
CIRCLE: 'value_type::circle',
},
};
mappings = measurementServiceMappingsFactory(measurementServiceMock);
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
csToolsAnnotation = {
toolName: definition,
measurementData: {
_measurementServiceId: 1,
sopInstanceUid: '123',
frameOfReferenceUid: '123',
seriesInstanceUid: '123',
handles,
text: 'Test',
description: 'Test',
unit: 'mm',
},
};
measurement = {
id: 1,
sopInstanceUID: '123',
frameOfReferenceUID: '123',
referenceSeriesUID: '123',
label: 'Test',
description: 'Test',
unit: 'mm',
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
points: points,
};
jest.clearAllMocks();
});

describe('toAnnotation()', () => {
it('map measurement service format to annotation', async () => {
const mappedMeasurement = await mappings.toAnnotation(measurement, definition);
expect(mappedMeasurement).toEqual(csToolsAnnotation);
});
});

describe('toMeasurement()', () => {
it('map annotation to measurement service format', async () => {
const mappedAnnotation = await mappings.toMeasurement(csToolsAnnotation);
expect(mappedAnnotation).toEqual(measurement);
});
});
});
Loading

0 comments on commit 0c37a40

Please sign in to comment.