fix(Measurements): in MeasurementService keep a separate set of unmapped measurements (#3385)
* fix(Measurements): return {} instead of undefined for getSOPInstanceAttributes
* PR feedback:
- separated measurements from non-acquisition plane
- reverted to previous behaviour of returning undefined in getSOPInstanceAttributes
* Added unit tests for unmapped measurements.
* PR feedback: added comments to the unmappedMeasurement used in the unit tests.
This commit is contained in:
parent
4fd81b5cd2
commit
4172d0427b
@ -3,39 +3,16 @@ import * as cornerstone from '@cornerstonejs/core';
|
|||||||
/**
|
/**
|
||||||
* It checks if the imageId is provided then it uses it to query
|
* It checks if the imageId is provided then it uses it to query
|
||||||
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
|
* the metadata and get the SOPInstanceUID, SeriesInstanceUID and StudyInstanceUID.
|
||||||
* If the imageId is not provided then it uses the sceneUID to get the viewports
|
* If the imageId is not provided then undefined is returned.
|
||||||
* inside the scene and then it checks each viewport to find the one that has
|
|
||||||
* acquisition plane view, and uses the currentImageId of the viewport to
|
|
||||||
* query the metadata and get UIDs.
|
|
||||||
* @param {string} imageId The image id of the referenced image
|
* @param {string} imageId The image id of the referenced image
|
||||||
* @param {string} sceneUID The scene UID of the measurement tool
|
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function getSOPInstanceAttributes(
|
export default function getSOPInstanceAttributes(imageId) {
|
||||||
imageId,
|
|
||||||
cornerstoneViewportService = undefined,
|
|
||||||
viewportId = undefined
|
|
||||||
) {
|
|
||||||
if (imageId) {
|
if (imageId) {
|
||||||
return _getUIDFromImageID(imageId);
|
return _getUIDFromImageID(imageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: implement for volume viewports and use the referencedSeriesInstanceUID
|
// Todo: implement for volume viewports and use the referencedSeriesInstanceUID
|
||||||
|
|
||||||
// if no imageId => measurement is not in the acquisition plane
|
|
||||||
// const metadata = getUIDFromScene(cornerstoneViewportService, viewportId);
|
|
||||||
|
|
||||||
// if (!metadata) {
|
|
||||||
// throw new Error('Not viewport with imageId found');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Since the series and study UID is derived from another viewport in the
|
|
||||||
// // same scene, we cannot include the SOPInstanceUID
|
|
||||||
// return {
|
|
||||||
// SOPInstanceUID: null,
|
|
||||||
// SeriesInstanceUID: metadata.SeriesInstanceUID,
|
|
||||||
// StudyInstanceUID: metadata.StudyInstanceUID,
|
|
||||||
// };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getUIDFromImageID(imageId) {
|
function _getUIDFromImageID(imageId) {
|
||||||
@ -48,23 +25,3 @@ function _getUIDFromImageID(imageId) {
|
|||||||
frameNumber: instance.frameNumber || 1,
|
frameNumber: instance.frameNumber || 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// function getUIDFromScene(cornerstoneViewportService) {
|
|
||||||
// const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
|
||||||
// const scene = renderingEngine.getScene(sceneUID);
|
|
||||||
|
|
||||||
// const viewportUIDs = scene.getViewportIds();
|
|
||||||
|
|
||||||
// if (viewportUIDs.length === 0) {
|
|
||||||
// throw new Error('No viewport found in scene');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (let i = 0; i < viewportUIDs.length; i++) {
|
|
||||||
// const vp = renderingEngine.getViewport(viewportUIDs[i]);
|
|
||||||
// const imageId = vp.getCurrentImageId();
|
|
||||||
|
|
||||||
// if (imageId) {
|
|
||||||
// return _getUIDFromImageID(imageId);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -8,13 +8,16 @@ jest.mock('../../log', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('MeasurementService.js', () => {
|
describe('MeasurementService.js', () => {
|
||||||
|
const unmappedMeasurementUID = 'unmappedMeasurementUId';
|
||||||
let measurementService;
|
let measurementService;
|
||||||
let measurement;
|
let measurement;
|
||||||
|
let unmappedMeasurement;
|
||||||
let source;
|
let source;
|
||||||
let annotationType;
|
let annotationType;
|
||||||
let matchingCriteria;
|
let matchingCriteria;
|
||||||
let toSourceSchema;
|
let toSourceSchema;
|
||||||
let toMeasurement;
|
let toMeasurement;
|
||||||
|
let toMeasurementThrowsError;
|
||||||
let annotation;
|
let annotation;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -40,6 +43,25 @@ describe('MeasurementService.js', () => {
|
|||||||
],
|
],
|
||||||
source: source,
|
source: source,
|
||||||
};
|
};
|
||||||
|
// A measurement with various metadata missing (e.g. referenced SOPInstanceUID) that
|
||||||
|
// would not typically get mapped my the MeasurementService possibly because it was
|
||||||
|
// made in a non-acquisition plane of a volume.
|
||||||
|
unmappedMeasurement = {
|
||||||
|
uid: unmappedMeasurementUID,
|
||||||
|
SOPInstanceUID: undefined,
|
||||||
|
FrameOfReferenceUID: undefined,
|
||||||
|
referenceSeriesUID: undefined,
|
||||||
|
label: 'Label',
|
||||||
|
description: 'Description',
|
||||||
|
unit: 'mm',
|
||||||
|
area: 123,
|
||||||
|
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
|
points: [
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
],
|
||||||
|
source: source,
|
||||||
|
};
|
||||||
toSourceSchema = () => annotation;
|
toSourceSchema = () => annotation;
|
||||||
toMeasurement = () => {
|
toMeasurement = () => {
|
||||||
if (Object.keys(measurement).includes('invalidProperty')) {
|
if (Object.keys(measurement).includes('invalidProperty')) {
|
||||||
@ -48,6 +70,9 @@ describe('MeasurementService.js', () => {
|
|||||||
|
|
||||||
return measurement;
|
return measurement;
|
||||||
};
|
};
|
||||||
|
toMeasurementThrowsError = () => {
|
||||||
|
throw new Error('Unmapped measurement.');
|
||||||
|
};
|
||||||
matchingCriteria = {
|
matchingCriteria = {
|
||||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
points: 2,
|
points: 2,
|
||||||
@ -429,5 +454,75 @@ describe('MeasurementService.js', () => {
|
|||||||
|
|
||||||
expect(updateCallbackWasCalled).toBe(false);
|
expect(updateCallbackWasCalled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('subscribers do NOT receive add unmapped measurements event', () => {
|
||||||
|
measurementService.addMapping(
|
||||||
|
source,
|
||||||
|
annotationType,
|
||||||
|
matchingCriteria,
|
||||||
|
toSourceSchema,
|
||||||
|
toMeasurementThrowsError
|
||||||
|
);
|
||||||
|
|
||||||
|
const { MEASUREMENT_ADDED } = measurementService.EVENTS;
|
||||||
|
let addCallbackWasCalled = false;
|
||||||
|
|
||||||
|
/* Subscribe to add event */
|
||||||
|
measurementService.subscribe(
|
||||||
|
MEASUREMENT_ADDED,
|
||||||
|
() => (addCallbackWasCalled = true)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Add new measurement - two calls needed for the start and the other for the completed*/
|
||||||
|
// expect exceptions for unmapped measurements
|
||||||
|
expect(() => {
|
||||||
|
source.annotationToMeasurement(annotationType, unmappedMeasurement);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
source.annotationToMeasurement(annotationType, {
|
||||||
|
unmappedMeasurementUID,
|
||||||
|
...unmappedMeasurement,
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
expect(addCallbackWasCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribers do receive remove unmapped measurements event', () => {
|
||||||
|
measurementService.addMapping(
|
||||||
|
source,
|
||||||
|
annotationType,
|
||||||
|
matchingCriteria,
|
||||||
|
toSourceSchema,
|
||||||
|
toMeasurementThrowsError
|
||||||
|
);
|
||||||
|
|
||||||
|
const { MEASUREMENT_REMOVED } = measurementService.EVENTS;
|
||||||
|
let removeCallbackWasCalled = false;
|
||||||
|
|
||||||
|
/* Subscribe to add event */
|
||||||
|
measurementService.subscribe(
|
||||||
|
MEASUREMENT_REMOVED,
|
||||||
|
() => (removeCallbackWasCalled = true)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Add new measurement - two calls needed for the start and the other for the completed*/
|
||||||
|
// expect exceptions for unmapped measurements
|
||||||
|
expect(() => {
|
||||||
|
source.annotationToMeasurement(annotationType, unmappedMeasurement);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
source.annotationToMeasurement(annotationType, {
|
||||||
|
unmappedMeasurementUID,
|
||||||
|
...unmappedMeasurement,
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
measurementService.remove(unmappedMeasurementUID);
|
||||||
|
|
||||||
|
expect(removeCallbackWasCalled).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -112,6 +112,7 @@ class MeasurementService extends PubSubService {
|
|||||||
public readonly VALUE_TYPES = VALUE_TYPES;
|
public readonly VALUE_TYPES = VALUE_TYPES;
|
||||||
|
|
||||||
private measurements = new Map();
|
private measurements = new Map();
|
||||||
|
private unmappedMeasurements = new Set();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(EVENTS);
|
super(EVENTS);
|
||||||
@ -532,6 +533,8 @@ class MeasurementService extends PubSubService {
|
|||||||
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
measurement = toMeasurementSchema(sourceAnnotationDetail);
|
||||||
measurement.source = source;
|
measurement.source = source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.unmappedMeasurements.add(sourceAnnotationDetail.uid);
|
||||||
|
|
||||||
console.log('Failed to map', error);
|
console.log('Failed to map', error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
|
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
|
||||||
@ -594,11 +597,16 @@ class MeasurementService extends PubSubService {
|
|||||||
* @param {MeasurementSource} source The measurement source instance
|
* @param {MeasurementSource} source The measurement source instance
|
||||||
*/
|
*/
|
||||||
remove(measurementUID, source, eventDetails) {
|
remove(measurementUID, source, eventDetails) {
|
||||||
if (!measurementUID || !this.measurements.has(measurementUID)) {
|
if (
|
||||||
|
!measurementUID ||
|
||||||
|
(!this.measurements.has(measurementUID) &&
|
||||||
|
!this.unmappedMeasurements.has(measurementUID))
|
||||||
|
) {
|
||||||
log.warn(`No uid provided, or unable to find measurement by uid.`);
|
log.warn(`No uid provided, or unable to find measurement by uid.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.unmappedMeasurements.delete(measurementUID);
|
||||||
this.measurements.delete(measurementUID);
|
this.measurements.delete(measurementUID);
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||||
source,
|
source,
|
||||||
@ -608,6 +616,8 @@ class MeasurementService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearMeasurements() {
|
clearMeasurements() {
|
||||||
|
this.unmappedMeasurements.clear();
|
||||||
|
|
||||||
// Make a copy of the measurements
|
// Make a copy of the measurements
|
||||||
const measurements = [...this.measurements.values()];
|
const measurements = [...this.measurements.values()];
|
||||||
this.measurements.clear();
|
this.measurements.clear();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user