fix(jump): Jump to measurement wasn't jumping when presentation already existed (#3318)
* fix: Jump to measurement after presentation store * Fix initial load of DICOM SR * Fix measurement highlight issues * Clear measurements changed to array * Fix merge issues * PR comments * Removed unused formatting. Change to single event * Fix the unit test * PR review comments * refactor(viewport): simplify imports, align destructuring Simplify import statements by removing unused imports, organize existing imports, and align destructured imports consistently across the component. ---------
This commit is contained in:
1 parent
66f6e3eade
commit
b5b936b53e
21 files changed
+640
-298
No files matched your search
@@ -67,7 +67,10 @@ const EVENTS = {
|
||||
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
||||
// Give the viewport a chance to jump to the measurement
|
||||
JUMP_TO_MEASUREMENT_VIEWPORT: 'event:jump_to_measurement_viewport',
|
||||
// Give the layout a chance to jump to the measurement
|
||||
JUMP_TO_MEASUREMENT_LAYOUT: 'event:jump_to_measurement_layout',
|
||||
};
|
||||
|
||||
const VALUE_TYPES = {
|
||||
@@ -104,15 +107,16 @@ class MeasurementService extends PubSubService {
|
||||
},
|
||||
};
|
||||
|
||||
public static readonly EVENTS = EVENTS;
|
||||
public static VALUE_TYPES = VALUE_TYPES;
|
||||
public readonly VALUE_TYPES = VALUE_TYPES;
|
||||
|
||||
private measurements = new Map();
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
this.sources = {};
|
||||
this.mappings = {};
|
||||
this.measurements = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +124,7 @@ class MeasurementService extends PubSubService {
|
||||
* This method should be used to add custom tool schema to the measurement service.
|
||||
* @param {Array} schema schema for validation
|
||||
*/
|
||||
addMeasurementSchemaKeys(schema) {
|
||||
public addMeasurementSchemaKeys(schema): void {
|
||||
if (!Array.isArray(schema)) {
|
||||
schema = [schema];
|
||||
}
|
||||
@@ -160,11 +164,7 @@ class MeasurementService extends PubSubService {
|
||||
* @return {Measurement[]} Array of measurements
|
||||
*/
|
||||
getMeasurements() {
|
||||
const measurements = this._arrayOfObjects(this.measurements);
|
||||
return (
|
||||
measurements &&
|
||||
measurements.map(m => this.measurements[Object.keys(m)[0]])
|
||||
);
|
||||
return [...this.measurements.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,18 +173,14 @@ class MeasurementService extends PubSubService {
|
||||
* @param {string} uid measurement uid
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
getMeasurement(measurementUID) {
|
||||
let measurement = null;
|
||||
const measurements = this.measurements[measurementUID];
|
||||
|
||||
if (measurements && Object.keys(measurements).length > 0) {
|
||||
measurement = this.measurements[measurementUID];
|
||||
}
|
||||
|
||||
return measurement;
|
||||
public getMeasurement(measurementUID: string) {
|
||||
return this.measurements.get(measurementUID);
|
||||
}
|
||||
|
||||
setMeasurementSelected(measurementUID, selected) {
|
||||
public setMeasurementSelected(
|
||||
measurementUID: string,
|
||||
selected: boolean
|
||||
): void {
|
||||
const measurement = this.getMeasurement(measurementUID);
|
||||
if (!measurement) {
|
||||
return;
|
||||
@@ -373,8 +369,8 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
}
|
||||
|
||||
update(measurementUID, measurement, notYetUpdatedAtSource = false) {
|
||||
if (!this.measurements[measurementUID]) {
|
||||
update(measurementUID: string, measurement, notYetUpdatedAtSource = false) {
|
||||
if (!this.measurements.has(measurementUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,7 +384,7 @@ class MeasurementService extends PubSubService {
|
||||
updatedMeasurement
|
||||
);
|
||||
|
||||
this.measurements[measurementUID] = updatedMeasurement;
|
||||
this.measurements.set(measurementUID, updatedMeasurement);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source: measurement.source,
|
||||
@@ -470,15 +466,15 @@ class MeasurementService extends PubSubService {
|
||||
uid: internalUID,
|
||||
};
|
||||
|
||||
if (this.measurements[internalUID]) {
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
if (this.measurements.get(internalUID)) {
|
||||
this.measurements.set(internalUID, newMeasurement);
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
});
|
||||
} else {
|
||||
log.info('Measurement added', newMeasurement);
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
this.measurements.set(internalUID, newMeasurement);
|
||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
@@ -557,7 +553,7 @@ class MeasurementService extends PubSubService {
|
||||
);
|
||||
}
|
||||
|
||||
const oldMeasurement = this.measurements[internalUID];
|
||||
const oldMeasurement = this.measurements.get(internalUID);
|
||||
|
||||
const newMeasurement = {
|
||||
...oldMeasurement,
|
||||
@@ -569,7 +565,7 @@ class MeasurementService extends PubSubService {
|
||||
if (oldMeasurement) {
|
||||
// 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.
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
this.measurements.set(internalUID, newMeasurement);
|
||||
if (isUpdate) {
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source,
|
||||
@@ -585,7 +581,7 @@ class MeasurementService extends PubSubService {
|
||||
}
|
||||
} else {
|
||||
log.info('Measurement started.', newMeasurement);
|
||||
this.measurements[internalUID] = newMeasurement;
|
||||
this.measurements.set(internalUID, newMeasurement);
|
||||
}
|
||||
|
||||
return newMeasurement.uid;
|
||||
@@ -598,12 +594,12 @@ class MeasurementService extends PubSubService {
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
*/
|
||||
remove(measurementUID, source, eventDetails) {
|
||||
if (!measurementUID || !this.measurements[measurementUID]) {
|
||||
if (!measurementUID || !this.measurements.has(measurementUID)) {
|
||||
log.warn(`No uid provided, or unable to find measurement by uid.`);
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.measurements[measurementUID];
|
||||
this.measurements.delete(measurementUID);
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||
source,
|
||||
measurement: measurementUID,
|
||||
@@ -613,9 +609,8 @@ class MeasurementService extends PubSubService {
|
||||
|
||||
clearMeasurements() {
|
||||
// Make a copy of the measurements
|
||||
const measurements = { ...this.measurements };
|
||||
this.measurements = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
const measurements = [...this.measurements.values()];
|
||||
this.measurements.clear();
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||
}
|
||||
|
||||
@@ -628,27 +623,39 @@ class MeasurementService extends PubSubService {
|
||||
this.clearMeasurements();
|
||||
}
|
||||
|
||||
jumpToMeasurement(viewportIndex, measurementUID) {
|
||||
const measurement = this.measurements[measurementUID];
|
||||
/**
|
||||
* This method calls the subscriptions for JUMP_TO_MEASUREMENT_VIEWPORT
|
||||
* and JUMP_TO_MEASUREMENT_LAYOUT. There are two events which are
|
||||
* fired because there are two different items which might want to handle
|
||||
* the event. First, there might already be a viewport which can handle
|
||||
* the event. If so, then the layout doesn't need to necessarily change.
|
||||
* This is communicated by the isConsumed value on the event itself.
|
||||
* Otherwise, the layout itself may need to be navigated to in order
|
||||
* to provide a viewport which can show the given measurement.
|
||||
*
|
||||
* When a viewport decides to apply the event, it should call the consume()
|
||||
* method on the event, so that other listeners know they do not need to
|
||||
* navigate. This does NOT affect whether the layout event is fired, and
|
||||
* merely causes it to fire the event with the isConsumed set to true.
|
||||
*/
|
||||
|
||||
public jumpToMeasurement(
|
||||
viewportIndex: number,
|
||||
measurementUID: string
|
||||
): void {
|
||||
const measurement = this.measurements.get(measurementUID);
|
||||
|
||||
if (!measurement) {
|
||||
log.warn(`No measurement uid, or unable to find by uid.`);
|
||||
return;
|
||||
}
|
||||
this._addJumpToMeasurement(viewportIndex, measurementUID);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.JUMP_TO_MEASUREMENT, {
|
||||
const consumableEvent = this.createConsumableEvent({
|
||||
viewportIndex,
|
||||
measurement,
|
||||
});
|
||||
}
|
||||
|
||||
getJumpToMeasurement(viewportIndex) {
|
||||
return this._jumpToMeasurementCache[viewportIndex];
|
||||
}
|
||||
|
||||
removeJumpToMeasurement(viewportIndex) {
|
||||
delete this._jumpToMeasurementCache[viewportIndex];
|
||||
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT, consumableEvent);
|
||||
this._broadcastEvent(EVENTS.JUMP_TO_MEASUREMENT_LAYOUT, consumableEvent);
|
||||
}
|
||||
|
||||
_getSourceUID(name, version) {
|
||||
@@ -663,10 +670,6 @@ class MeasurementService extends PubSubService {
|
||||
return sourceUID;
|
||||
}
|
||||
|
||||
_addJumpToMeasurement(viewportIndex, measurementUID) {
|
||||
this._jumpToMeasurementCache[viewportIndex] = measurementUID;
|
||||
}
|
||||
|
||||
_getMappingByMeasurementSource(measurement, annotationType) {
|
||||
if (this._isValidSource(measurement.source)) {
|
||||
return this.mappings[measurement.source.uid].find(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
const EVENTS = {
|
||||
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
|
||||
LAYOUT_CHANGED: 'event::layoutChanged',
|
||||
GRID_STATE_CHANGED: 'event::gridStateChanged',
|
||||
};
|
||||
import { getPresentationIds, PresentationIds } from './getPresentationIds';
|
||||
|
||||
class ViewportGridService extends PubSubService {
|
||||
public static readonly EVENTS = {
|
||||
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
|
||||
LAYOUT_CHANGED: 'event::layoutChanged',
|
||||
GRID_STATE_CHANGED: 'event::gridStateChanged',
|
||||
};
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'viewportGridService',
|
||||
altName: 'ViewportGridService',
|
||||
@@ -14,12 +15,13 @@ class ViewportGridService extends PubSubService {
|
||||
return new ViewportGridService();
|
||||
},
|
||||
};
|
||||
public static EVENTS = EVENTS;
|
||||
|
||||
public static getPresentationIds = getPresentationIds;
|
||||
|
||||
serviceImplementation = {};
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
super(ViewportGridService.EVENTS);
|
||||
this.serviceImplementation = {};
|
||||
}
|
||||
|
||||
@@ -37,10 +39,12 @@ class ViewportGridService extends PubSubService {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
if (setActiveViewportIndexImplementation) {
|
||||
this.serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation;
|
||||
this.serviceImplementation._setActiveViewportIndex =
|
||||
setActiveViewportIndexImplementation;
|
||||
}
|
||||
if (setDisplaySetsForViewportsImplementation) {
|
||||
this.serviceImplementation._setDisplaySetsForViewports = setDisplaySetsForViewportsImplementation;
|
||||
this.serviceImplementation._setDisplaySetsForViewports =
|
||||
setDisplaySetsForViewportsImplementation;
|
||||
}
|
||||
if (setLayoutImplementation) {
|
||||
this.serviceImplementation._setLayout = setLayoutImplementation;
|
||||
@@ -55,7 +59,8 @@ class ViewportGridService extends PubSubService {
|
||||
this.serviceImplementation._set = setImplementation;
|
||||
}
|
||||
if (getNumViewportPanesImplementation) {
|
||||
this.serviceImplementation._getNumViewportPanes = getNumViewportPanesImplementation;
|
||||
this.serviceImplementation._getNumViewportPanes =
|
||||
getNumViewportPanesImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +80,29 @@ class ViewportGridService extends PubSubService {
|
||||
|
||||
public setDisplaySetsForViewport(props) {
|
||||
// Just update a single viewport, but use the multi-viewport update for it.
|
||||
this.serviceImplementation._setDisplaySetsForViewports([props]);
|
||||
this.setDisplaySetsForViewports([props]);
|
||||
}
|
||||
|
||||
public setDisplaySetsForViewports(props) {
|
||||
this.serviceImplementation._setDisplaySetsForViewports(props);
|
||||
const state = this.getState();
|
||||
const viewports = [];
|
||||
|
||||
for (const viewport of props) {
|
||||
const updatedViewport = state.viewports[viewport.viewportIndex];
|
||||
if (updatedViewport) {
|
||||
viewports.push(updatedViewport);
|
||||
} else {
|
||||
console.warn(
|
||||
"ViewportGridService::Didn't find updated viewport",
|
||||
viewport
|
||||
);
|
||||
}
|
||||
}
|
||||
this._broadcastEvent(ViewportGridService.EVENTS.GRID_STATE_CHANGED, {
|
||||
state,
|
||||
viewports,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,3 +173,5 @@ class ViewportGridService extends PubSubService {
|
||||
}
|
||||
|
||||
export default ViewportGridService;
|
||||
|
||||
export type { PresentationIds };
|
||||
@@ -0,0 +1,121 @@
|
||||
const JOIN_STR = '&';
|
||||
|
||||
// The default lut presentation id if none defined
|
||||
const DEFAULT = 'default';
|
||||
|
||||
// This code finds the first unique index to add to the presentation id so that
|
||||
// two viewports containing the same display set in the same type of viewport
|
||||
// can have different presentation information. This allows comparison of
|
||||
// a single display set in two or more viewports, when the user has simply
|
||||
// dragged and dropped the view in twice. For example, it allows displaying
|
||||
// bone, brain and soft tissue views of a single display set, and to still
|
||||
// remember the specific changes to each viewport.
|
||||
const addUniqueIndex = (arr, key, viewports) => {
|
||||
arr.push(0);
|
||||
// The 128 is just a value that is larger than how many viewports we
|
||||
// display at once, used as an upper bound on how many unique presentation
|
||||
// ID's might exist for a single display set at once.
|
||||
for (let displayInstance = 0; displayInstance < 128; displayInstance++) {
|
||||
arr[arr.length - 1] = displayInstance;
|
||||
const testId = arr.join(JOIN_STR);
|
||||
if (
|
||||
!viewports.find(
|
||||
viewport => viewport.viewportOptions?.presentationIds?.[key] === testId
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getLutId = (ds): string => {
|
||||
if (!ds || !ds.options) return DEFAULT;
|
||||
if (ds.options.id) return ds.options.id;
|
||||
const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${val}`);
|
||||
if (!arr.length) return DEFAULT;
|
||||
return arr.join(JOIN_STR);
|
||||
};
|
||||
|
||||
export type PresentationIds = {
|
||||
positionPresentationId?: string;
|
||||
lutPresentationId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a set of presentation IDs for a viewport. The presentation IDs are
|
||||
* used to remember the presentation state of the viewport when it is navigated
|
||||
* to different layouts.
|
||||
*
|
||||
* The design of this is setup to allow preserving the view information in the
|
||||
* following cases:
|
||||
*
|
||||
*
|
||||
* * If a set of display sets was previously displayed in the same initial
|
||||
* position as it is currently being asked to be displayed,
|
||||
* then remember the camera position as previously displayed
|
||||
*
|
||||
* * If a set of display sets was previously displayed with the same initial
|
||||
* LUT conditions, then remember the last LUT displayed for that display set
|
||||
* and re-apply it.
|
||||
*
|
||||
* * Otherwise, apply the initial hanging protocol specified LUT and camera
|
||||
* position to new display sets.
|
||||
*
|
||||
* This means generating two presentationId keys:
|
||||
*
|
||||
* `positionPresentationId`
|
||||
*
|
||||
* Used for getting the camera/initial position state sync values.
|
||||
* This is a combination of:
|
||||
* * `viewportOptions.id`
|
||||
* * `viewportOptions.orientation`
|
||||
* * display set UID's - as displayed for this viewport, excluding seg
|
||||
* * a unique index number if the previous key is already displayed
|
||||
*
|
||||
* `lutPresentationId`
|
||||
*
|
||||
* Used for getting the voi LUT information. Generated from:
|
||||
*
|
||||
* * `displaySetOption[0].options` - including the id if present
|
||||
* * displaySetUID's
|
||||
* * a unique index number if the previously generated key is already
|
||||
* displayed.
|
||||
*
|
||||
* @param viewport requiring a presentation Id
|
||||
* @param viewports is the list of viewports being shown. Any presentation ID's
|
||||
* among them must not be re-used in order to have each viewport have it's own presentation ID.
|
||||
* @returns PresentationIds
|
||||
*/
|
||||
const getPresentationIds = (viewport, viewports): PresentationIds => {
|
||||
if (!viewport) return;
|
||||
const {
|
||||
viewportOptions,
|
||||
displaySetInstanceUIDs,
|
||||
displaySetOptions,
|
||||
} = viewport;
|
||||
if (!viewportOptions || !displaySetInstanceUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, orientation } = viewportOptions;
|
||||
const lutId = getLutId(displaySetOptions[0]);
|
||||
const lutPresentationArr = [lutId];
|
||||
|
||||
const positionPresentationArr = [orientation || 'acquisition'];
|
||||
if (id) positionPresentationArr.push(id);
|
||||
|
||||
for (const uid of displaySetInstanceUIDs) {
|
||||
positionPresentationArr.push(uid);
|
||||
lutPresentationArr.push(uid);
|
||||
}
|
||||
|
||||
addUniqueIndex(positionPresentationArr, 'positionPresentationId', viewports);
|
||||
addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports);
|
||||
|
||||
const lutPresentationId = lutPresentationArr.join(JOIN_STR);
|
||||
const positionPresentationId = positionPresentationArr.join(JOIN_STR);
|
||||
return { lutPresentationId, positionPresentationId };
|
||||
};
|
||||
|
||||
export default getPresentationIds;
|
||||
export { getPresentationIds };
|
||||
@@ -103,4 +103,20 @@ export class PubSubService {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.unsubscriptions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event that records whether or not someone
|
||||
* has consumed it. Call eventData.consume() to consume the event.
|
||||
* Check eventData.isConsumed to see if it is consumed or not.
|
||||
* @param props - to include in the event
|
||||
*/
|
||||
protected createConsumableEvent(props) {
|
||||
return {
|
||||
...props,
|
||||
isConsumed: false,
|
||||
consume: function Consume() {
|
||||
this.isConsumed = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,27 +24,22 @@ describe('absoluteUrl', () => {
|
||||
});
|
||||
|
||||
test('should return the original path when there path in the window.origin after the domain and port', () => {
|
||||
global.window = Object.create(window);
|
||||
delete global.window.location;
|
||||
const url = 'http://dummy.com';
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
origin: url,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
global.window.location = {
|
||||
origin: url,
|
||||
};
|
||||
const absoluteUrlOutput = absoluteUrl('path_1/path_2/path_3');
|
||||
expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3');
|
||||
});
|
||||
|
||||
test('should be able to return the absolute path even when the path contains duplicates', () => {
|
||||
global.window = Object.create(window);
|
||||
global.window ||= Object.create(window);
|
||||
const url = 'http://dummy.com';
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: {
|
||||
origin: url,
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
delete global.window.location;
|
||||
global.window.location = {
|
||||
origin: url,
|
||||
};
|
||||
const absoluteUrlOutput = absoluteUrl('path_1/path_1/path_1');
|
||||
expect(absoluteUrlOutput).toEqual('/path_1/path_1/path_1');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user