fix: Clicking on measurement should highlight the measurement row in the measurement panel OHIF-433 (#3111)

* fix: OHIF-433 Clicking on measurement should highlight the measurement row in the measurement panel

Added selected flag to each OHIF measurement.
Listening to CS3D ANNOTATION_SELECTION_CHANGE to keep the selected flag in sync.

* fix: OHIF-433 Clicking on measurement should highlight the measurement row in the measurement panel

Fixed broken unit tests.

* PR feedback from Alireza:
- use MEASUREMENT_UPDATED event for selection changed
- added TODO to eventually have CS3D introduce the selected flag per annotation
- added MeasurementService._publishEvent

* PR feedback: now using _broadcastEvent
This commit is contained in:
Joe Boccanfuso 2023-01-13 13:57:18 -05:00 committed by GitHub
parent a997198587
commit 58ae8690a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 36 deletions

View File

@ -121,11 +121,36 @@ const connectToolsToMeasurementService = (
const { toolName } = metadata; const { toolName } = metadata;
annotationModifiedEventDetail.uid = annotationUID; annotationModifiedEventDetail.uid = annotationUID;
annotationToMeasurement(toolName, annotationModifiedEventDetail); // Passing true to indicate this is an update and NOT a annotation (start) completion.
annotationToMeasurement(toolName, annotationModifiedEventDetail, true);
} catch (error) { } catch (error) {
console.warn('Failed to update measurement:', error); console.warn('Failed to update measurement:', error);
} }
} }
function selectMeasurement(csToolsEvent) {
try {
const annotationSelectionEventDetail = csToolsEvent.detail;
const {
added: addedSelectedAnnotationUIDs,
removed: removedSelectedAnnotationUIDs,
} = annotationSelectionEventDetail;
if (removedSelectedAnnotationUIDs) {
removedSelectedAnnotationUIDs.forEach(annotationUID =>
MeasurementService.setMeasurementSelected(annotationUID, false)
);
}
if (addedSelectedAnnotationUIDs) {
addedSelectedAnnotationUIDs.forEach(annotationUID =>
MeasurementService.setMeasurementSelected(annotationUID, true)
);
}
} catch (error) {
console.warn('Failed to select and unselect measurements:', error);
}
}
/** /**
* When csTools fires a removed event, remove the same measurement * When csTools fires a removed event, remove the same measurement
@ -157,13 +182,17 @@ const connectToolsToMeasurementService = (
// on display sets added, check if there are any measurements in measurement service that need to be // on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools // put into cornerstone tools
const addedEvt = csToolsEvents.ANNOTATION_ADDED;
const completedEvt = csToolsEvents.ANNOTATION_COMPLETED; const completedEvt = csToolsEvents.ANNOTATION_COMPLETED;
const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED; const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED;
const removedEvt = csToolsEvents.ANNOTATION_REMOVED; const removedEvt = csToolsEvents.ANNOTATION_REMOVED;
const selectionEvt = csToolsEvents.ANNOTATION_SELECTION_CHANGE;
eventTarget.addEventListener(addedEvt, addMeasurement);
eventTarget.addEventListener(completedEvt, addMeasurement); eventTarget.addEventListener(completedEvt, addMeasurement);
eventTarget.addEventListener(updatedEvt, updateMeasurement); eventTarget.addEventListener(updatedEvt, updateMeasurement);
eventTarget.addEventListener(removedEvt, removeMeasurement); eventTarget.addEventListener(removedEvt, removeMeasurement);
eventTarget.addEventListener(selectionEvt, selectMeasurement);
return csTools3DVer1MeasurementSource; return csTools3DVer1MeasurementSource;
}; };

View File

@ -86,7 +86,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];

View File

@ -85,7 +85,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];

View File

@ -91,7 +91,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
const targets = Object.keys(cachedStats); const targets = Object.keys(cachedStats);
if (!targets.length) { if (!targets.length) {
return; return [];
} }
const annotations = []; const annotations = [];

View File

@ -255,14 +255,13 @@ function _getMappedMeasurements(MeasurementService) {
} }
function _mapMeasurementToDisplay(measurement, index, types) { function _mapMeasurementToDisplay(measurement, index, types) {
const { displayText, uid, label, type } = measurement; const { displayText, uid, label, type, selected } = measurement;
return { return {
uid, uid,
label: label || '(empty)', label: label || '(empty)',
measurementType: type, measurementType: type,
displayText: displayText || [], displayText: displayText || [],
// TODO: handle one layer down isActive: selected,
isActive: false, // activeMeasurementItem === i + 1,
}; };
} }

View File

@ -323,7 +323,7 @@ function _mapMeasurementToDisplay(measurement, types, DisplaySetService) {
label: measurement.label || '(empty)', label: measurement.label || '(empty)',
measurementType: measurement.type, measurementType: measurement.type,
displayText: displayText || [], displayText: displayText || [],
isActive: false, // activeMeasurementItem === i + 1, isActive: measurement.selected,
}; };
} }

View File

@ -26,6 +26,7 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
* @property {number} area - * @property {number} area -
* @property {Array} points - * @property {Array} points -
* @property {MeasurementSource} source - * @property {MeasurementSource} source -
* @property {boolean} selected -
*/ */
/* Measurement schema keys for object validation. */ /* Measurement schema keys for object validation. */
@ -56,6 +57,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'shortestDiameter', 'shortestDiameter',
'longestDiameter', 'longestDiameter',
'cachedStats', 'cachedStats',
'selected',
]; ];
const EVENTS = { const EVENTS = {
@ -184,6 +186,20 @@ class MeasurementService {
return measurement; return measurement;
} }
setMeasurementSelected(measurementUID, selected) {
const measurement = this.getMeasurement(measurementUID);
if (!measurement) {
return;
}
measurement.selected = selected;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
source: measurement.source,
measurement,
});
}
/** /**
* Create a new source. * Create a new source.
* *
@ -218,8 +234,17 @@ class MeasurementService {
version, version,
}; };
source.annotationToMeasurement = (annotationType, annotation) => { source.annotationToMeasurement = (
return this.annotationToMeasurement(source, annotationType, annotation); annotationType,
annotation,
isUpdate = false
) => {
return this.annotationToMeasurement(
source,
annotationType,
annotation,
isUpdate
);
}; };
source.remove = (measurementUID, eventDetails) => { source.remove = (measurementUID, eventDetails) => {
@ -467,9 +492,15 @@ class MeasurementService {
* @param {MeasurementSource} source The measurement source instance * @param {MeasurementSource} source The measurement source instance
* @param {string} annotationType The source annotationType * @param {string} annotationType The source annotationType
* @param {EventDetail} sourceAnnotationDetail for the annotation event * @param {EventDetail} sourceAnnotationDetail for the annotation event
* @param {boolean} isUpdate is this an update or an add/completed instead?
* @return {string} A measurement uid * @return {string} A measurement uid
*/ */
annotationToMeasurement(source, annotationType, sourceAnnotationDetail) { annotationToMeasurement(
source,
annotationType,
sourceAnnotationDetail,
isUpdate = false
) {
if (!this._isValidSource(source)) { if (!this._isValidSource(source)) {
throw new Error('Invalid source.'); throw new Error('Invalid source.');
} }
@ -524,19 +555,27 @@ class MeasurementService {
}; };
if (this.measurements[internalUID]) { if (this.measurements[internalUID]) {
// TODO: Ultimately, each annotation should have a selected flag right from the soure.
// For now, it is just added in OHIF here and in setMeasurementSelected.
newMeasurement.selected = this.measurements[internalUID].selected;
this.measurements[internalUID] = newMeasurement; this.measurements[internalUID] = newMeasurement;
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, { if (isUpdate) {
source, this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
measurement: newMeasurement, source,
notYetUpdatedAtSource: false, measurement: newMeasurement,
}); notYetUpdatedAtSource: false,
});
} else {
log.info('Measurement added.', newMeasurement);
this.measurements[internalUID] = newMeasurement;
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
source,
measurement: newMeasurement,
});
}
} else { } else {
log.info('Measurement added.', newMeasurement); log.info('Measurement started.', newMeasurement);
this.measurements[internalUID] = newMeasurement; this.measurements[internalUID] = newMeasurement;
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
source,
measurement: newMeasurement,
});
} }
return newMeasurement.uid; return newMeasurement.uid;
@ -588,16 +627,10 @@ class MeasurementService {
} }
this._addJumpToMeasurement(viewportIndex, measurementUID); this._addJumpToMeasurement(viewportIndex, measurementUID);
const eventName = this.EVENTS.JUMP_TO_MEASUREMENT; this._broadcastEvent(this.EVENTS.JUMP_TO_MEASUREMENT, {
viewportIndex,
const hasListeners = Object.keys(this.listeners).length > 0; measurement,
const hasCallbacks = Array.isArray(this.listeners[eventName]); });
if (hasListeners && hasCallbacks) {
this.listeners[eventName].forEach(listener => {
listener.callback({ viewportIndex, measurement });
});
}
} }
getJumpToMeasurement(viewportIndex) { getJumpToMeasurement(viewportIndex) {

View File

@ -364,8 +364,9 @@ describe('MeasurementService.js', () => {
() => (addCallbackWasCalled = true) () => (addCallbackWasCalled = true)
); );
/* Add new measurement */ /* Add new measurement - two calls needed for the start and the other for the completed*/
source.annotationToMeasurement(annotationType, measurement); const uid = source.annotationToMeasurement(annotationType, measurement);
source.annotationToMeasurement(annotationType, { uid, ...measurement });
expect(addCallbackWasCalled).toBe(true); expect(addCallbackWasCalled).toBe(true);
}); });
@ -392,7 +393,11 @@ describe('MeasurementService.js', () => {
const uid = source.annotationToMeasurement(annotationType, measurement); const uid = source.annotationToMeasurement(annotationType, measurement);
/* Update measurement */ /* Update measurement */
source.annotationToMeasurement(annotationType, { uid, ...measurement }); source.annotationToMeasurement(
annotationType,
{ uid, ...measurement },
true
);
expect(updateCallbackWasCalled).toBe(true); expect(updateCallbackWasCalled).toBe(true);
}); });
@ -418,8 +423,9 @@ describe('MeasurementService.js', () => {
/* Unsubscribe */ /* Unsubscribe */
unsubscribe(); unsubscribe();
/* Create measurement */ /* Create measurement - two calls needed one to start and one to complete */
source.annotationToMeasurement(annotationType, measurement); const uid = source.annotationToMeasurement(annotationType, measurement);
source.annotationToMeasurement(annotationType, { uid, ...measurement });
expect(updateCallbackWasCalled).toBe(false); expect(updateCallbackWasCalled).toBe(false);
}); });