feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)

* feat: add initial sop class handler for SEG

* feat: move segmentation service

* update segmentation service methods

* fix: viewport data structure

* feat: Add initial render for the DICOM SEG for each displaySet

* fix: various wrong architectural dependencies between services

* feat: initial separate SEG display in each viewport

* feat: refactore viewport action bar

* initial work for SEG hydration

* fix: various bugs regarding drag and dropping different viewports

* fix: rendering issues for multiple seg displaysets

* fix: bugs for thumbnail and hydration

* feat: fix the initial segment color

* update after rebase

* feat: initial design for the segmentation group table

* feat: initial new design for the segmentation panel

* feat: segmentation panel

* feat: make segmentation appear on all related viewports

* fix: segmentation load bug based on functional groups

* initial work for segmentation crosshairs

* fix: various stylings and functionality

* fix: various stylings for the seg panel

* fix: overflow styles

* feat: add more ui components

* feat: added segmentation config

* feat: add jump to segment

* feat: add jump to segment for DICOM SEG viewport

* fix: bugs after rebase

* feat: jump in segmentation viewport

* fix: mpr support for seg

* feat: add more icons

* feat: new icons

* feat: add new side panel

* feat: add segmentation config

* feat: add loading indicator to ohif

* feat: make hanging protocols follow matching rules for viewports

* feat: enhance drag and drop to be hanging protocol aware

* fix: mpr restore previous layout

* fix: crosshairs toggle

* fix: add auth headers to the dicom loader via dicomwebclient

* fix: bug for crosshairs toggle in mpr

* fix: seg viewport reusing old toolGroup

* feat: add loading animation with lottie

* fix: various bugs for Segmentation hydration in mpr

* feat: change outline alpha to outline opacity

* feat: loading indicator for seg viewport

* fix: various segmentation group styles

* fix: local mode for seg

* feat: add animation for the highlight

* fix: loading indicatro to show segment indices

* feat: enhance modality drop down ui

* fix: panels

* fix: download form

* fix: layout shift in loading indicator

* fix: loading indicator to have correct values

* fix: update software number

* fix: image jump between MPR and default

* fix: segmentation cleanup and cine service cleanup

* fix: segmentation toolgroup clena up

* fix: highlight interval should not trigger again

* fix: issue with multiframe sorting

* rename: change onSeriesChange to onArrowsClick

* fix: buttons for tmtv and layout shift

* fix: various bugs wrt crosshairs

* fix: crosshairs re init on reset camera

* fix: middle slice calculation different from cs middle reset camera

* fix: reset camera should reset viewport camera

* fix: loading segments in MPR mode

* fix: tmtv hp back to before

* fix: layout shift

* feat: orientation markers for volume viewport

* fix: capture for volume viewports

* fix: memory leak for back to worklist

* fix: loading bar bg color

* fix: side panel for only one panel

* fix: react select style in production

* fix: various styling for segmentation groups

* fix: various ui styles

* feat: add new segmentation config styles

* fix: hover state for segmentation item

* fix: side panel layout shift

* temp add panels

* try to fix scrollbar for thimbnail

* fix: scroll not appearing for side panels

* feat: changed styles for scrollbar

* feat: add cpu fallback warning

* fix: webworker destroy

* fix: select styles

* fix: orientation marker color and position

* fix: capture screenshot for volume viewports

* fix: hide segment visibility on mpr

* fix: reloading an already loaded seg displayset

* feat: add is equal

* fix: mpr jump to measurements

* apply review comments

* fix: optimization for the segmentation load

* fix: remove unnecessary context menu and hp service reset

* fix: segmentation service wrt brush settings

* feat: initial work for the stack synchronization

* fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements

* fix: various react proptypes

* fix: thumbnails for the tmtv and change default props to default params

* feat: make showing loading indicator configurable and add docs

* bump versions and add more docs

* fix demo

* update cornerstone versions

* apply review comments

* feat: add reference lines

* fix build

* fix unit tests

* add reference lines icon

* fix: e2e tests

* fix static wado config

* docs: add segmentation service docs

* fix: docker build

* fix: keep camera and bump versions

* Try re-enabling minification to fix deploy previews

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
AlirezaandErik Ziegler authored and GitHub committed 2022-11-16 14:19:52 -05:00
1 parent 08c58b0634
commit 83ea228d86
254 files changed
+12856 -3141

No files matched your search

+1
View File
@@ -45,6 +45,7 @@
"moment": "^2.24.0",
"query-string": "^6.14.0",
"object-hash": "2.1.1",
"gl-matrix": "^3.4.3",
"validate.js": "^0.12.0"
},
"devDependencies": {
-1
View File
@@ -33,7 +33,6 @@ describe('Top level exports', () => {
'ToolBarService',
'Types',
'ViewportGridService',
'SegmentationService',
'HangingProtocolService',
'UserAuthenticationService',
'IWebApiDataSource',
-3
View File
@@ -27,7 +27,6 @@ import {
HangingProtocolService,
pubSubServiceInterface,
UserAuthenticationService,
SegmentationService,
} from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
@@ -68,7 +67,6 @@ const OHIF = {
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
SegmentationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,
@@ -104,7 +102,6 @@ export {
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
SegmentationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,
@@ -124,9 +124,9 @@ describe('HangingProtocolService', () => {
it('matches best image match', () => {
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
expect(hpAlreadyApplied).toMatchObject([false]);
expect(viewportMatchDetails.length).toBe(1);
expect(viewportMatchDetails[0]).toMatchObject({
expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]]));
expect(viewportMatchDetails.size).toBe(1);
expect(viewportMatchDetails.get(0)).toMatchObject({
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
@@ -1,3 +1,4 @@
import uuidv4 from '../../utils/uuidv4';
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy';
import ProtocolEngine from './ProtocolEngine';
@@ -11,9 +12,11 @@ const EVENTS = {
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
CUSTOM_IMAGE_LOAD_PERFORMED:
'event::hanging_protocol_custom_image_load_performed',
HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT:
'event::hanging_protocol_applied_for_viewport',
};
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
type Protocol = HangingProtocol.Protocol;
class HangingProtocolService {
studies: StudyMetadata[];
@@ -27,7 +30,6 @@ class HangingProtocolService {
_commandsManager: Record<string, unknown>;
_servicesManager: Record<string, unknown>;
protocolEngine: ProtocolEngine;
hpAlreadyApplied: boolean[] = [];
customViewportSettings = [];
displaySets: IDisplaySet[] = [];
activeStudy: Record<string, unknown>;
@@ -54,19 +56,30 @@ class HangingProtocolService {
return prev;
}, []),
},
isReconstructable: {
name: 'Checks if the display set is reconstructable',
// we can add more advanced checking here
callback: displaySet => displaySet.isReconstructable ?? false,
},
};
listeners = {};
registeredImageLoadStrategies = {};
activeImageLoadStrategyName = null;
customImageLoadPerformed = false;
/**
* Whether the hanging protocol is applied for the given viewport index,
* applying means that the displayset(s) is set for the viewport to be shown
*/
hpAlreadyApplied: Map<number, boolean> = new Map();
/**
* displaySetMatchDetails = <displaySetId, match>
* DisplaySetId is the id defined in the hangingProtocol object itself
* and match is an object that contains information about
*/
displaySetMatchDetails: Map<
string,
string, // protocol displaySetId in the displayset selector
HangingProtocol.DisplaySetMatchDetails
> = new Map();
@@ -74,7 +87,10 @@ class HangingProtocolService {
* An array that contains for each viewport (viewportIndex) specified in the
* hanging protocol, an object of the form
*/
viewportMatchDetails = [] as HangingProtocol.ViewportMatchDetails[];
viewportMatchDetails: Map<
number, // viewportIndex
HangingProtocol.ViewportMatchDetails
> = new Map();
constructor(commandsManager, servicesManager) {
this._commandsManager = commandsManager;
@@ -94,12 +110,16 @@ class HangingProtocolService {
Object.assign(this, pubSubServiceInterface);
}
public destroy() {
this.reset();
this.protocols = new Map();
}
public reset() {
this.studies = [];
this.protocols = new Map();
this.hpAlreadyApplied = [];
this.viewportMatchDetails = [];
// this.ProtocolEngine.reset()
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
this.displaySetMatchDetails = new Map();
}
public getDefaultProtocol(): HangingProtocol.Protocol {
@@ -148,21 +168,7 @@ class HangingProtocolService {
public getProtocolById(id: string): HangingProtocol.Protocol {
const protocol = this.protocols.get(id);
if (protocol instanceof Function) {
try {
const { protocol: generatedProtocol } = this._getProtocolFromGenerator(
protocol
);
return generatedProtocol;
} catch (error) {
console.warn(
`Error while executing protocol generator for protocol ${id}: ${error}`
);
}
} else {
return protocol;
}
return protocol;
}
/**
@@ -207,9 +213,7 @@ class HangingProtocolService {
* Sets the active hanging protocols to use, by name. If the value is empty,
* then resets the active protocols to all the named items.
*/
public setActiveProtocols(
hangingProtocol?: string[] | string
): void {
public setActiveProtocols(hangingProtocol?: string[] | string): void {
if (!hangingProtocol || !hangingProtocol.length) {
this.activeProtocolIds = null;
console.log('No active protocols, setting all to active');
@@ -289,8 +293,21 @@ class HangingProtocolService {
}
}
public setHangingProtocolAppliedForViewport(i): void {
this.hpAlreadyApplied[i] = true;
setHangingProtocolAppliedForViewport(i, status, suppressEvent = false) {
this.hpAlreadyApplied.set(i, status);
const numberOfViewports = this.viewportMatchDetails.size;
const numberOfViewportsApplied = Array.from(
this.hpAlreadyApplied.values()
).filter(applied => applied).length;
const progress = Math.round(
(numberOfViewportsApplied / numberOfViewports) * 100
);
this._broadcastChange(this.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT, {
progress: progress,
});
}
/**
@@ -405,55 +422,181 @@ class HangingProtocolService {
return protocol;
}
getViewportsRequireUpdate(viewportIndex, displaySetInstanceUID) {
const newDisplaySetInstanceUID = displaySetInstanceUID;
const protocol = this.protocol;
const protocolStage = protocol.stages[this.stage];
const protocolViewports = protocolStage.viewports;
const protocolViewport = protocolViewports[viewportIndex];
const defaultReturn = [
{
viewportIndex,
displaySetInstanceUIDs: [newDisplaySetInstanceUID],
},
];
// if no viewport, then we can assume there is no predefined set of
// rules that should be applied to this viewport while matching
if (!protocolViewport) {
return defaultReturn;
}
// no support for drag and drop into fusion viewports yet
// Todo: smart drag and drop would look at the displaySets and
// replace the same modality type, but later
if (protocolViewport.displaySets.length > 1) {
throw new Error('Cannot update viewport with multiple displaySets yet');
}
// If there is no displaySet, then we can assume that the viewport
// is empty and we can just add the new displaySet to it
if (protocolViewport.displaySets.length === 0) {
return defaultReturn;
}
// if the viewport is not empty, then we check the displaySets it is showing
// currently, which means we need to check if the requested updated displaySet
// follow the same rules as the current displaySets
const displaySetSelectorId = protocolViewport.displaySets[0].id;
const displaySetSelector =
protocol.displaySetSelectors[displaySetSelectorId];
if (!displaySetSelector) {
return defaultReturn;
}
// so let's check if the new displaySetInstanceUIDs follow the same rules
this._validateViewportSpecificMatch(
{
displaySetInstanceUIDs: [newDisplaySetInstanceUID],
viewportOptions: {},
displaySetOptions: [],
},
protocolViewport,
protocol.displaySetSelectors
);
// if we reach here, it means there are some rules that should be applied
// if we don't have any match details for the displaySetSelector the viewport
// is currently showing, then we can assume that the new displaySetInstanceUID
// does not
if (!this.displaySetMatchDetails.get(displaySetSelectorId)) {
return defaultReturn;
}
// if we reach here, it means that the displaySetInstanceUIDs to be dropped
// in the viewportIndex are valid, and we can proceed with the update. However
// we need to check if the displaySets that the viewport were showing
// was also referenced by other viewports, and if so, we need to update those
// viewports as well
// check if displaySetSelectors are used by other viewports, and
// store the viewportIndex and displaySetInstanceUIDs that need to be updated
const viewportsToUpdate = [];
protocolViewports.forEach((viewport, index) => {
let viewportNeedsUpdate;
for (const displaySet of viewport.displaySets) {
if (displaySet.id === displaySetSelectorId) {
viewportNeedsUpdate = true;
break;
}
}
if (viewportNeedsUpdate) {
// we can then loop over the displaySets and choose all of them,
// but for the one that matches the oldDisplaySetInstanceUID we need to
// replace it with the newDisplaySetInstanceUID
const {
displaySetInstanceUIDs,
displaySetOptions,
} = viewport.displaySets.reduce(
(acc, displaySet) => {
const { id, options } = displaySet;
let {
displaySetInstanceUID: displaySetInstanceUIDToUse,
} = this.displaySetMatchDetails.get(id);
if (displaySet.id === displaySetSelectorId) {
displaySetInstanceUIDToUse = newDisplaySetInstanceUID;
}
acc.displaySetInstanceUIDs.push(displaySetInstanceUIDToUse);
acc.displaySetOptions.push(options);
return acc;
},
{ displaySetInstanceUIDs: [], displaySetOptions: [] }
);
viewportsToUpdate.push({
viewportIndex: index,
displaySetInstanceUIDs,
viewportOptions: viewport.viewportOptions,
displaySetOptions,
});
}
});
return viewportsToUpdate;
}
/**
* It applied the protocol to the current studies and display sets based on the
* protocolId that is provided.
* @param protocolId - name of the protocol to be set
* @param protocol - protocol object (optional), if not provided, the protocol
* will be retrieved from the list of protocols by its name
* @param matchingDisplaySets - predefined display sets to be used for the protocol
* @param protocolId - name of the registered protocol to be set
* @param options - options to be passed to the protocol, this is either an array
* of the displaySetInstanceUIDs to be set on ALL VIEWPORTS OF THE PROTOCOL or an object
* that contains viewportIndex as the key and displaySetInstanceUIDs as the value
* for each viewport that needs to be set.
* @param errorCallback - callback to be called if there is an error
* during the protocol application
*
* @returns boolean - true if the protocol was applied and no errors were found
*/
public setProtocol(
protocolId: string,
protocol?: HangingProtocol.Protocol,
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
options = {} as HangingProtocol.SetProtocolOptions,
errorCallback = null
): void {
if (!protocol) {
const foundProtocol = this.protocols.get(protocolId);
const foundProtocol = this.protocols.get(protocolId);
if (!foundProtocol) {
console.warn(
`HangingProtocolService::setProtocol - protocol ${protocolId} not found`
);
return;
}
if (foundProtocol instanceof Function) {
try {
({ protocol, matchingDisplaySets } = this._getProtocolFromGenerator(
foundProtocol
));
} catch (error) {
console.warn(
`HangingProtocolService::setProtocol - protocol ${protocolId} failed to execute`,
error
);
return;
}
} else {
protocol = foundProtocol;
}
if (!foundProtocol) {
console.warn(
`ProtocolEngine::setProtocol - Protocol with id ${protocolId} not found - you should register it first via addProtocol`
);
return;
}
this._setProtocol(protocol, matchingDisplaySets);
const protocol = this._validateProtocol(foundProtocol);
if (options) {
this._validateOptions(options);
}
try {
this._setProtocol(protocol, options);
} catch (error) {
console.error(error);
if (errorCallback) {
errorCallback(error);
}
throw new Error(error);
}
}
private _setProtocol(
protocol: HangingProtocol.Protocol,
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
options = null as HangingProtocol.SetProtocolOptions
): void {
this.stage = 0;
this.protocol = protocol;
const oldProtocol = this.protocol;
this.protocol = this._copyProtocol(protocol);
const { imageLoadStrategy } = protocol;
if (imageLoadStrategy) {
// check if the imageLoadStrategy is a valid strategy
@@ -464,12 +607,19 @@ class HangingProtocolService {
this.activeImageLoadStrategyName = imageLoadStrategy;
}
}
this._updateViewports(matchingDisplaySets);
try {
this._updateViewports(options);
} catch (error) {
this.protocol = oldProtocol;
throw new Error(error);
}
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
viewportMatchDetails: this.viewportMatchDetails,
displaySetMatchDetails: this.displaySetMatchDetails,
hpAlreadyApplied: this.hpAlreadyApplied,
protocol: this.protocol,
});
}
@@ -498,48 +648,42 @@ class HangingProtocolService {
return this.protocol.stages[this.stage];
}
private _getProtocolFromGenerator(
protocolGenerator: HangingProtocol.ProtocolGenerator
): {
protocol: HangingProtocol.Protocol;
matchingDisplaySets: Record<string, HangingProtocol.DisplaySetMatchDetails>;
} {
const { protocol, matchingDisplaySets } = protocolGenerator({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
});
const validatedProtocol = this._validateProtocol(protocol);
return {
protocol: validatedProtocol,
matchingDisplaySets,
};
}
/**
* Updates the viewports with the selected protocol stage.
*/
_updateViewports(
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
): void {
_updateViewports(options = null as HangingProtocol.SetProtocolOptions): void {
// Make sure we have an active protocol with a non-empty array of display sets
if (!this._getNumProtocolStages()) {
console.log('No protocol stages - nothing to display');
return;
throw new Error('No protocol or stages found');
}
// each time we are updating the viewports, we need to reset the
// matching applied
this.hpAlreadyApplied = [];
// reset displaySetMatchDetails
// Todo: we can have more intelligent invalidation of the hpAlreadyApplied
// since sometimes we are just updating some viewports and the rest are
// already applied (e.g. when we are using the drag and drop)
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
this.displaySetMatchDetails = new Map();
this.customImageLoadPerformed = false;
if (matchingDisplaySets) {
this.displaySetMatchDetails = new Map(
Object.entries(matchingDisplaySets)
);
if (options) {
// if the options are defined, we can fill in the displaySetMatchDetails
// but we need to also check that any displaySetInstanceUIDs that are
// provided either at viewport level or at the protocol level SATISFIES
// the required seriesMatching criteria species in the protocol. Otherwise
// we need to throw an error. This way protocols become more strict and
// for instance don't allow drag and drop of displaySets that don't match
// (in MPR protocol we specify in the displaysetSelector that the displaySet
// to be used should be reconstructable. Or you can specify that the displaySet
// should be a localizer only for a protocol)
// options can be either an object with { displaySetInstanceUIDs, viewportOptions, displaySetOptions }
// options (global options), or an object of objects with viewportIndex
// as the key and the { displaySetInstanceUIDs, viewportOptions, displaySetOptions } as the value
// The following function will update the displaySetMatchDetails in place
this._updateMatchByOptions(this.protocol, options);
}
const { displaySetSelectors = {} } = this.protocol;
@@ -559,9 +703,7 @@ class HangingProtocolService {
return;
}
this.customImageLoadPerformed = false;
const { layoutType } = stageModel.viewportStructure;
// Retrieve the properties associated with the current display set's viewport structure template
// If no such layout properties exist, stop here.
const layoutProps = stageModel.viewportStructure.properties;
@@ -580,10 +722,21 @@ class HangingProtocolService {
});
// Matching the displaySets
for (const viewport of stageModel.viewports) {
for (
let viewportIndex = 0;
viewportIndex < numCols * numRows;
viewportIndex++
) {
if (viewportIndex >= stageModel.viewports.length) {
// If we have more viewports than display sets, stop here.
break;
}
const viewport = stageModel.viewports[viewportIndex];
for (const displaySet of viewport.displaySets) {
const { id: displaySetId } = displaySet;
// skip matching if already matched
// skip matching if already matched (e.g. by options above)
if (this.displaySetMatchDetails.has(displaySetId)) {
continue;
}
@@ -606,9 +759,8 @@ class HangingProtocolService {
// Loop through each viewport
stageModel.viewports.forEach((viewport, viewportIndex) => {
const { viewportOptions } = viewport;
this.hpAlreadyApplied.push(false);
const { viewportOptions = {} } = viewport;
this.hpAlreadyApplied.set(viewportIndex, false);
// DisplaySets for the viewport, Note: this is not the actual displaySet,
// but it is a info to locate the displaySet from the displaySetService
const displaySetsInfo = [];
@@ -646,13 +798,328 @@ class HangingProtocolService {
}
);
this.viewportMatchDetails[viewportIndex] = {
this.viewportMatchDetails.set(viewportIndex, {
viewportOptions,
displaySetsInfo,
};
});
});
}
_updateMatchByOptions(
protocol: Protocol,
options: HangingProtocol.SetProtocolOptions
) {
const { DisplaySetService } = this._servicesManager.services;
if (options.displaySetInstanceUIDs) {
this._updateGlobalMatchByOptions(
options as HangingProtocol.GlobalProtocolOptions,
protocol,
DisplaySetService
);
return;
}
// Todo: I don't think we need the following anymore, since the drag and
// drop has been reworked to ask HangingProtocolService to check
// which viewports need to be updated. I don't think there is other use cases
// other than drag and drop and thumbnails double click that will specify
// options at viewport level. if there are the following code will need to be
// uncommented and tested.
this._updateViewportSpecificMatchByOptions(
options as HangingProtocol.ViewportSpecificProtocolOptions,
protocol
);
}
private _updateViewportSpecificMatchByOptions(
options: HangingProtocol.ViewportSpecificProtocolOptions,
protocol: HangingProtocol.Protocol
) {
const { DisplaySetService } = this._servicesManager.services;
const { displaySetSelectors = {} } = protocol;
const protocolViewports = protocol.stages[this.stage].viewports;
// if we get here, we can fill in the displaySetMatchDetails
for (const viewportIndex in options) {
const displaySetAndViewportOptions = options[viewportIndex];
const protocolViewport = protocolViewports[viewportIndex];
// if the protocol already has the viewport
if (protocolViewport) {
// if the protocol has a viewport with specific displaySets, we need to check if the
// displaySetInstanceUIDs are allowed by the protocol
this._validateViewportSpecificMatch(
displaySetAndViewportOptions,
protocolViewport,
displaySetSelectors
);
displaySetAndViewportOptions.displaySetInstanceUIDs.forEach(
(displaySetInstanceUID, index) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const displaySetId = protocolViewport.displaySets[index].id;
// since we are setting the displaySetMatchDetails for each viewport
// directly, a side effect is that any other viewport that is referencing
// the same displaySetId will also be updated with the new
// displaySetMatchDetails, but sometimes this is not a desired behavior
// which is why we are using the syncDataForViewports to determine if
// we should update the displaySetMatchDetails for all the viewports
// that are affected by the syncDataForViewports
if (protocol.syncDataForViewports) {
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
} else {
// if the protocol does not have the syncDataForViewports, we need to
// update the displaySetMatchDetails by introducing the displaySetIndex
// to the displaySetMatchDetails. This way we can match the displaySetInstanceUIDs
// for the viewportIndex, but also for the other viewports that are affected
// by the syncDataForViewports
const displaySetSelectorId = `${displaySetId}_${uuidv4()}`;
// update the displaySetId at the viewport
protocolViewport.displaySets[index].id = displaySetSelectorId;
this.displaySetMatchDetails.set(displaySetSelectorId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
}
}
);
} else {
// if the protocol does not have the viewport, we need to create it
const newViewport = {
displaySets: [],
viewportOptions: {},
};
displaySetAndViewportOptions?.displaySetInstanceUIDs?.forEach(
(displaySetInstanceUID, index) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const displaySetId = `viewport_${viewportIndex}_displaySet_${index}`;
newViewport.displaySets.push({
id: displaySetId,
});
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
}
);
protocolViewports[viewportIndex] = newViewport;
}
}
}
private _validateViewportSpecificMatch(
displaySetAndViewportOptions: HangingProtocol.DisplaySetAndViewportOptions,
protocolViewport: HangingProtocol.Viewport,
displaySetSelectors: Record<string, HangingProtocol.DisplaySetSelector>
) {
const { DisplaySetService } = this._servicesManager.services;
const protocolViewportDisplaySets = protocolViewport.displaySets;
const numDisplaySetsToSet =
displaySetAndViewportOptions.displaySetInstanceUIDs.length;
if (
protocolViewportDisplaySets.length > 0 &&
numDisplaySetsToSet !== protocolViewportDisplaySets.length
) {
throw new Error(
`The number of displaySets to set ${numDisplaySetsToSet} does not match the number of displaySets in the protocol ${protocolViewportDisplaySets} - not currently implemented`
);
}
displaySetAndViewportOptions.displaySetInstanceUIDs.forEach(
displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const { displaySets: displaySetsInfo } = protocolViewport;
for (const displaySetInfo of displaySetsInfo) {
const displaySetSelector = displaySetSelectors[displaySetInfo.id];
if (!displaySetSelector) {
continue;
}
this._validateRequiredSelectors(displaySetSelector, displaySet);
}
}
);
}
private _updateGlobalMatchByOptions(
options: HangingProtocol.GlobalProtocolOptions,
protocol: Protocol,
DisplaySetService: any
) {
const { displaySetSelectors = {} } = protocol;
const protocolViewports = protocol.stages[this.stage].viewports;
options = options as HangingProtocol.GlobalProtocolOptions;
// we need to check each displaySetInstanceUIDs to see if it satisfies the
// seriesMatching criteria
options.displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// match against all the displaySetSelectors defined in the protocol
for (const displaySetId in displaySetSelectors) {
const displaySetSelector = displaySetSelectors[displaySetId];
this._validateRequiredSelectors(displaySetSelector, displaySet);
}
});
// if we get here, we can fill in the displaySetMatchDetails
// however, there might be a case where we are asked to
// render multiple displaySets for a single viewport
// so we need to go back to viewports and start from there
const newDisplaySetIds: Set<string> = new Set();
// Todo: this currently work for current stage only
protocolViewports.forEach(viewport => {
viewport.displaySets.forEach(displaySetInfo => {
const { id: displaySetId } = displaySetInfo;
if (!this.displaySetMatchDetails.has(displaySetId)) {
newDisplaySetIds.add(displaySetId);
}
});
});
// Todo: handle override of the viewport and displaySet options
Array.from(newDisplaySetIds).forEach((displaySetId, index) => {
const displaySetInstanceUID = options.displaySetInstanceUIDs[index];
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
});
/*
Todo: make it work for the case where the number of displaySets
to set is not equal to the number of displaySets in the protocol, we can modify
the protocol to have the same number of displaySets for each viewport
if (newDisplaySetIds.size !== numberOfDisplaySetsToSet) {
// the remaining ones to set
const remainingDisplaySetMatches = options.slice(
newDisplaySetIds.size,
numberOfDisplaySetsToSet
);
protocol[this.stage].viewports.forEach(viewport => {
const { displaySets } = viewport;
// push the displaySetsToSet to the end of the displaySets array
remainingDisplaySetMatches.forEach(({ displaySetInstanceUID }) => {
displaySets.push({
id: displaySetInstanceUID,
});
});
});
remainingDisplaySetMatches.forEach(({ displaySetInstanceUID }) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
this.displaySetMatchDetails.set(displaySetInstanceUID, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
});
}
// Todo: this has a bug where if the protocol defines only one displaySet,
// but renders a fusion, it cannot go back to one displaySet since its
// viewport.displaySets.length is 2 and it should remove the second one
// later
*/
}
private _validateRequiredSelectors(
displaySetSelector: HangingProtocol.DisplaySetSelector,
displaySet: any
) {
const { seriesMatchingRules } = displaySetSelector;
if (seriesMatchingRules.length) {
// only match the required rules
const requiredRules = seriesMatchingRules.filter(rule => rule.required);
const matched = this.protocolEngine.findMatch(displaySet, requiredRules);
if (!matched || matched.score === 0) {
throw new Error(
`The displaySetInstanceUID ${displaySet.displaySetInstanceUID} does not satisfy the required seriesMatching criteria for the protocol`
);
}
}
}
_validateOptions(options: HangingProtocol.SetProtocolOptions) {
const { DisplaySetService } = this._servicesManager.services;
if (options.displaySetInstanceUIDs) {
options = options as HangingProtocol.GlobalProtocolOptions;
options.displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (!displaySet) {
throw new Error(
`The displaySetInstanceUID ${displaySetInstanceUID} is not found in the displaySetService`
);
}
});
}
}
// Match images given a list of Studies and a Viewport's image matching reqs
_matchImages(displaySetRules) {
// TODO: matching is applied on study and series level, instance
@@ -821,12 +1288,12 @@ class HangingProtocolService {
* Changes the current stage to a new stage index in the display set sequence.
* It checks if the next stage exists.
*
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
* @param {Integer} stageAction An integer value specifying whether next (1) or previous (-1) stage
* @return {Boolean} True if new stage has set or false, otherwise
*/
_setCurrentProtocolStage(stageAction): boolean {
//reseting the applied protocols
this.hpAlreadyApplied = [];
//resetting the applied protocols
this.hpAlreadyApplied = new Map();
// Check if previous or next stage is available
if (stageAction === -1 && !this._isPreviousStageAvailable()) {
return false;
@@ -883,6 +1350,142 @@ class HangingProtocolService {
});
}
}
_copyProtocol(protocol: Protocol) {
return JSON.parse(JSON.stringify(protocol));
}
/**
_setProtocolLayoutOptions(protocol: Protocol, { numRows, numCols }) {
const layoutOptions = [];
const protocolStage = protocol.stages[this.stage];
const numViewports = protocolStage.viewports.length;
for (let i = 0; i < numViewports; i++) {
const { row, col } = unravelIndex(i, numRows, numCols);
const w = 1 / numCols;
const h = 1 / numRows;
const xPos = col * w;
const yPos = row * h;
layoutOptions[i] = {
width: w,
height: h,
x: xPos,
y: yPos,
};
}
// Todo: handle the case where the viewportStructure is not a grid
protocolStage.viewportStructure.properties.rows = numRows;
protocolStage.viewportStructure.properties.columns = numCols;
protocolStage.viewportStructure.properties.layoutOptions = {
...layoutOptions,
};
}
_getUpdatedProtocol({
numRows,
numCols,
protocol: oldProtocol,
}: {
numRows: number;
numCols: number;
protocol: Protocol;
}): Protocol {
let newProtocol = this._copyProtocol(oldProtocol);
const protocolStage = newProtocol.stages[this.stage];
// The following commented code is a potential improvements to the
// hanging protocols to intelligently switch between number of rows
// and columns based on the old state of the protocol. For instance,
// changing from 2x2 to 2x3 (adding a column) right now reorders the viewports
// as well, however, it should just add one empty column to the right
// and leave the rest of the viewports in place. This sounds amazing,
// but comes at a cost (which we need to tackle later). The cost is that
// the viewportIndex will change during this smart change of layout. In
// the example above the viewport at index (2) bottom left, will now be
// at index (3) bottom left, and since react will re-render the viewport
// it will reset the viewport's state such as (zoom, pan, windowLevel, imageIndex)
// and the user will lose their current state. In addition, all our viewportIds
// are dependent on the viewportIndex, so we will need to update all the viewportIds
// as well, and you can see how this can get out of hand. Later, we should
// tackle this problem and make the smart change of layout work.
// const { rows: oldNumRows, columns: oldNumCols } = newProtocol.stages[
// this.stage
// ].viewportStructure.properties;
// const oldToNewViewportIndices = getGridMapping(
// {
// numRows: oldNumRows,
// numCols: oldNumCols,
// },
// {
// numRows,
// numCols,
// }
// );
const protocolViewports = protocolStage.viewports;
if (protocolViewports.length < numRows * numCols) {
const newViewports = [];
for (let i = protocolViewports.length; i < numRows * numCols; i++) {
newViewports.push({
viewportOptions: {
toolGroupId: 'default',
viewportType: 'stack',
},
displaySets: [
{
id: `viewport-${i}`,
},
],
});
}
protocolStage.viewports = [...protocolViewports, ...newViewports];
} else if (protocolViewports.length > numRows * numCols) {
// remove viewports that are not needed
protocolStage.viewports = protocolViewports.slice(0, numRows * numCols);
}
// update the displaySetMatchDetails to reflect the new viewports
const toRemove = [];
this.displaySetMatchDetails.forEach(
(displaySetMatchDetail, displaySetId) => {
// if the displaySetId is not referenced in the protocolStage viewports
// we can remove it
const found = protocolStage.viewports.find(viewport => {
return viewport.displaySets.find(displaySet => {
return displaySet.id === displaySetId;
});
});
if (!found) {
toRemove.push(displaySetId);
}
}
);
toRemove.forEach(displaySetId => {
this.displaySetMatchDetails.delete(displaySetId);
});
this._setProtocolLayoutOptions(newProtocol, { numRows, numCols });
newProtocol = this._validateProtocol(newProtocol);
// Todo: not sure if we need to reset here, or we can smartly update the
// viewportMatchDetails and hpAlreadyApplied maps
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
return newProtocol;
}
*/
}
export default HangingProtocolService;
@@ -0,0 +1,356 @@
// This can be calculated by some formula probably, but for now we just use a constant since
// this might be objective
const GRID_MAPPINGS = {
// 1x2
'1x2:1x1': {
0: 0,
},
'1x2:1x3': {
0: 0,
1: 1,
},
'1x2:2x1': {
0: 0,
1: 1,
},
'1x2:2x2': {
0: 0,
1: 1,
},
'1x2:2x3': {
0: 0,
1: 1,
},
'1x2:3x1': {
0: 0,
1: 1,
},
'1x2:3x2': {
0: 0,
1: 1,
},
'1x2:3x3': {
0: 0,
1: 1,
},
// 1x3
'1x3:1x1': {
0: 0,
},
'1x3:1x2': {
0: 0,
1: 1,
},
'1x3:2x1': {
0: 0,
1: 1,
},
'1x3:2x2': {
0: 0,
1: 1,
2: 2,
},
'1x3:2x3': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x1': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x2': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x3': {
0: 0,
1: 1,
2: 2,
},
// 2x1
'2x1:1x2': {
0: 0,
1: 1,
},
'2x1:1x3': {
0: 0,
1: 1,
},
'2x1:2x2': {
0: 0,
2: 1,
},
'2x1:2x3': {
0: 0,
3: 1,
},
'2x1:3x1': {
0: 0,
1: 1,
},
'2x1:3x2': {
0: 0,
2: 1,
},
'2x1:3x3': {
0: 0,
3: 1,
},
// 2x2
'2x2:1x2': {
0: 0,
1: 1,
},
'2x2:1x3': {
0: 0,
1: 1,
2: 2,
},
'2x2:2x1': {
0: 0,
1: 2,
},
'2x2:2x3': {
0: 0,
1: 1,
3: 2,
4: 3,
},
'2x2:3x1': {
0: 0,
1: 1,
2: 2,
},
'2x2:3x2': {
0: 0,
1: 1,
2: 2,
3: 3,
},
'2x2:3x3': {
0: 0,
1: 1,
3: 2,
4: 3,
},
// 2x3
'2x3:1x2': {
0: 0,
1: 1,
},
'2x3:1x3': {
0: 0,
1: 1,
2: 2,
},
'2x3:2x1': {
0: 0,
1: 3,
},
'2x3:2x2': {
0: 0,
1: 1,
2: 3,
3: 4,
},
'2x3:3x1': {
0: 0,
1: 1,
2: 2,
},
'2x3:3x2': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'2x3:3x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
// 3x1
'3x1:1x2': {
0: 0,
1: 1,
},
'3x1:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x1:2x1': {
0: 0,
1: 1,
},
// TODO: I'm not sure about the following
'3x1:2x2': {
0: 0,
1: 1,
2: 2,
},
'3x1:2x3': {
0: 0,
1: 1,
2: 2,
},
'3x1:3x2': {
0: 0,
2: 1,
4: 2,
},
'3x1:3x3': {
0: 0,
3: 1,
6: 2,
},
// 3x2
'3x2:1x2': {
0: 0,
1: 1,
},
'3x2:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x2:2x1': {
0: 0,
1: 2,
},
'3x2:2x2': {
0: 0,
1: 1,
2: 2,
3: 3,
},
'3x2:2x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'3x2:3x1': {
0: 0,
1: 2,
2: 4,
},
'3x2:3x3': {
0: 0,
1: 1,
3: 2,
4: 3,
6: 4,
7: 5,
},
// 3x3
'3x3:1x2': {
0: 0,
1: 1,
},
'3x3:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x3:2x1': {
0: 0,
1: 3,
},
'3x3:2x2': {
0: 0,
1: 1,
2: 3,
3: 4,
},
'3x3:2x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'3x3:3x1': {
0: 0,
1: 3,
2: 6,
},
'3x3:3x2': {
0: 0,
1: 1,
2: 3,
3: 4,
4: 6,
5: 7,
},
};
/**
* The purpose of this function is to convert a grid with numRows and numCols
* and index for each cell into another grid with different dimensions, but it should
* intelligently use the data from the original grid to fill the new grid at
* correct locations.
*
* For instance:
* if the old grid is a 2x2 (numRows = 2, numCols = 2) and the new grid is a 3x3
* it should intelligently insert the cells in the new grid so that the cells
* are added to the right most column. Then the mapping is as follows:
* 0 -> 0, 1 -> 1, 3 -> 2, 4 -> 3 (viewport 2 in the old grid can be used in
* the place of viewport 3 in the new grid)
*
* Or if the old grid is 2x2 and new grid is 2x4, the mapping is as follows:
* 0 -> 0, 1 -> 1, 4 -> 2 and 5 -> 3
*
* Or if the old grid is 2x2 and the new grid is 1x2, the mapping is as follows:
* 0 -> 0, 1 -> 2
*
* @param {Object} oldGrid
* @param {number} oldGrid.numRows
* @param {number} oldGrid.numCols
*
* @param {Object} newGrid
* @param {number} newGrid.numRows
* @param {number} newGrid.numCols
*
* @returns {Map} A map that maps the new indices to the old indices
*
*/
const getGridMapping = (oldGrid, newGrid) => {
const mapping = {};
const { numRows: oldNumRows, numCols: oldNumCols } = oldGrid;
const { numRows: newNumRows, numCols: newNumCols } = newGrid;
if (oldNumRows === 1 && oldNumCols === 1) {
// If the old grid is 1x1, then we can just return the first cell
mapping[0] = 0;
return mapping;
}
if (newNumRows === 1 && newNumCols === 1) {
// If the new grid is 1x1, then we can just return the first cell
mapping[0] = 0;
return mapping;
}
const key = `${oldNumRows}x${oldNumCols}:${newNumRows}x${newNumCols}`;
const map = GRID_MAPPINGS[key];
if (!map) {
throw new Error(`No mapping found for ${key}`);
}
return map;
};
export default getGridMapping;
@@ -1,136 +0,0 @@
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
const EVENTS = {
SEGMENTATION_UPDATED: 'event::segmentation_updated',
SEGMENTATION_ADDED: 'event::segmentation_added',
SEGMENTATION_REMOVED: 'event::segmentation_removed',
SEGMENTATION_VISIBILITY_CHANGED: 'event::SEGMENTATION_VISIBILITY_CHANGED',
};
const VALUE_TYPES = {};
class SegmentationService {
constructor() {
this.segmentations = {};
this.listeners = {};
Object.defineProperty(this, 'EVENTS', {
value: EVENTS,
writable: false,
enumerable: true,
configurable: false,
});
Object.assign(this, pubSubServiceInterface);
}
/**
* Get all segmentations.
*
* @return Array of segmentations
*/
getSegmentations() {
const segmentations = this._arrayOfObjects(this.segmentations);
return (
segmentations &&
segmentations.map(m => this.segmentations[Object.keys(m)[0]])
);
}
/**
* Get specific segmentation by its id.
*
* @param id If of the segmentation
* @return segmentation instance
*/
getSegmentation(id) {
return this.segmentations[id];
}
addOrUpdateSegmentation(
id,
segmentationSchema,
notYetUpdatedAtSource = false
) {
const segmentation = this.segmentations[id];
if (segmentation) {
Object.assign(segmentation, segmentationSchema);
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
id,
segmentation,
notYetUpdatedAtSource: notYetUpdatedAtSource,
});
return;
}
this.segmentations[id] = {
...segmentationSchema,
visible: true,
};
this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
id,
segmentation,
});
}
/**
* Toggles the visibility of a segmentation in the state, and broadcasts the event.
* Note: this method does not update the segmentation state in the source. It only
* updates the state, and there should be separate listeners for that.
* @param ids segmentation ids
*/
toggleSegmentationsVisibility(ids) {
ids.forEach(id => {
const segmentation = this.segmentations[id];
if (!segmentation) {
throw new Error(`Segmentation with id ${id} not found.`);
}
segmentation.visible = !segmentation.visible;
this._broadcastEvent(this.EVENTS.SEGMENTATION_VISIBILITY_CHANGED, {
segmentation,
});
});
}
/**
* Removes a segmentation and broadcasts the removed event.
*
* @param {string} id The segmentation id
*/
remove(id) {
if (!id || !this.segmentations[id]) {
console.warn(`No id provided, or unable to find segmentation by id.`);
return;
}
delete this.segmentations[id];
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
id,
});
}
/**
* Clear all segmentations and broadcasts cleared event.
*/
clear() {
Object.keys(this.segmentations).forEach(id => {
this.remove(id);
});
this.segmentations = {};
}
/**
* Converts object of objects to array.
*
* @return {Array} Array of objects
*/
_arrayOfObjects = obj => {
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
};
}
export default SegmentationService;
export { EVENTS, VALUE_TYPES };
@@ -1,8 +0,0 @@
import SegmentationService from './SegmentationService';
export default {
name: 'SegmentationService',
create: ({ configuration = {} }) => {
return new SegmentationService();
},
};
@@ -70,21 +70,34 @@ export default class ToolBarService {
break;
}
case 'tool': {
this.state.primaryToolId = itemId;
commands.forEach(
({ commandName = 'setToolActive', commandOptions, context }) => {
commandsManager.runCommand(commandName, commandOptions, context);
}
);
try {
commands.forEach(
({ commandName = 'setToolActive', commandOptions, context }) => {
commandsManager.runCommand(commandName, commandOptions, context);
}
);
// only set the primary tool if no error was thrown
this.state.primaryToolId = itemId;
} catch (error) {
console.warn(error);
}
break;
}
case 'toggle': {
const { commands } = interaction;
let commandExecuted;
// only toggle if a command was executed
this.state.toggles[itemId] =
this.state.toggles[itemId] === undefined
? true
: !this.state.toggles[itemId];
const { commands } = interaction;
if (!commands) {
break;
}
commands.forEach(({ commandName, commandOptions, context }) => {
if (!commandOptions) {
@@ -93,9 +106,21 @@ export default class ToolBarService {
if (commandName) {
commandOptions.toggledState = this.state.toggles[itemId];
commandsManager.runCommand(commandName, commandOptions, context);
try {
commandsManager.runCommand(commandName, commandOptions, context);
commandExecuted = true;
} catch (error) {
console.warn(error);
}
}
});
if (!commandExecuted) {
// If no command was executed, we need to toggle the state back
this.state.toggles[itemId] = !this.state.toggles[itemId];
}
break;
}
default:
@@ -1,95 +0,0 @@
const name = 'ViewportGridService';
const publicAPI = {
name,
getState: _getState,
setActiveViewportIndex: _setActiveViewportIndex,
setDisplaySetsForViewport: _setDisplaySetsForViewport,
setLayout: _setLayout,
setCachedLayout: _setCachedLayout,
setServiceImplementation,
reset: _reset,
set: _set,
};
const serviceImplementation = {
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
_setActiveViewportIndex: () =>
console.warn('setActiveViewportIndex() NOT IMPLEMENTED'),
_setDisplaySetsForViewport: () =>
console.warn('setDisplaySetsForViewport() NOT IMPLEMENTED'),
_setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'),
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
_setCachedLayout: () => console.warn('setCachedLayout() NOT IMPLEMENTED'),
_set: () => console.warn('set() NOT IMPLEMENTED'),
};
function _getState() {
return serviceImplementation._getState();
}
function _setActiveViewportIndex(index) {
return serviceImplementation._setActiveViewportIndex(index);
}
function _setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUIDs }) {
return serviceImplementation._setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
});
}
function _setLayout({ numCols, numRows }) {
return serviceImplementation._setLayout({ numCols, numRows });
}
function _set(state) {
return serviceImplementation._set(state);
}
function _reset() {
return serviceImplementation._reset({});
}
function _setCachedLayout({ numCols, numRows, viewports }) {
return serviceImplementation._setLayout({ numCols, numRows, viewports });
}
function setServiceImplementation({
getState: getStateImplementation,
setActiveViewportIndex: setActiveViewportIndexImplementation,
setDisplaySetsForViewport: setDisplaySetsForViewportImplementation,
setCachedLayout: setCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
serviceImplementation._getState = getStateImplementation;
}
if (setActiveViewportIndexImplementation) {
serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation;
}
if (setDisplaySetsForViewportImplementation) {
serviceImplementation._setDisplaySetsForViewport = setDisplaySetsForViewportImplementation;
}
if (setLayoutImplementation) {
serviceImplementation._setLayout = setLayoutImplementation;
}
if (resetImplementation) {
serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) {
serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
}
if (setImplementation) {
serviceImplementation._set = setImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -0,0 +1,113 @@
import pubSubServiceInterface from './../_shared/pubSubServiceInterface';
const EVENTS = {
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
};
class ViewportGridService {
serviceImplementation = {};
EVENTS: { [key: string]: string };
listeners = {};
constructor() {
Object.assign(this, pubSubServiceInterface);
this.serviceImplementation = {};
this.EVENTS = EVENTS;
}
setServiceImplementation({
getState: getStateImplementation,
setActiveViewportIndex: setActiveViewportIndexImplementation,
setDisplaySetsForViewport: setDisplaySetsForViewportImplementation,
setDisplaySetsForViewports: setDisplaySetsForViewportsImplementation,
setCachedLayout: setCachedLayoutImplementation,
restoreCachedLayout: restoreCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setActiveViewportIndexImplementation) {
this.serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation;
}
if (setDisplaySetsForViewportImplementation) {
this.serviceImplementation._setDisplaySetsForViewport = setDisplaySetsForViewportImplementation;
}
if (setDisplaySetsForViewportsImplementation) {
this.serviceImplementation._setDisplaySetsForViewports = setDisplaySetsForViewportsImplementation;
}
if (setLayoutImplementation) {
this.serviceImplementation._setLayout = setLayoutImplementation;
}
if (resetImplementation) {
this.serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) {
this.serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
}
if (restoreCachedLayoutImplementation) {
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
}
if (setImplementation) {
this.serviceImplementation._set = setImplementation;
}
}
public setActiveViewportIndex(index) {
this.serviceImplementation._setActiveViewportIndex(index);
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED, {
viewportIndex: index,
});
}
public getState() {
return this.serviceImplementation._getState();
}
public setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
}) {
this.serviceImplementation._setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
});
}
public setDisplaySetsForViewports(viewports) {
this.serviceImplementation._setDisplaySetsForViewports(viewports);
}
public setLayout({ numCols, numRows }) {
this.serviceImplementation._setLayout({ numCols, numRows });
}
public reset() {
this.serviceImplementation._reset();
}
public setCachedLayout({ cacheId, cachedLayout }) {
this.serviceImplementation._setCachedLayout({ cacheId, cachedLayout });
}
public restoreCachedLayout(cacheId) {
this.serviceImplementation._restoreCachedLayout(cacheId);
}
public set(state) {
this.serviceImplementation._set(state);
}
}
export default {
name: 'ViewportGridService',
create: ({ configuration = {} }) => {
return new ViewportGridService();
},
};
-2
View File
@@ -12,7 +12,6 @@ import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
import SegmentationService from './SegmentationService';
export {
MeasurementService,
@@ -29,5 +28,4 @@ export {
CineService,
pubSubServiceInterface,
UserAuthenticationService,
SegmentationService,
};
+27 -8
View File
@@ -18,10 +18,28 @@ type DisplaySetMatchDetails = {
sortingInfo?: any;
};
type DisplaySetAndViewportOptions = {
displaySetInstanceUIDs: string[];
viewportOptions: ViewportOptions;
displaySetOptions: DisplaySetOptions;
}
type ViewportSpecificProtocolOptions = {
[viewportIndex: string]: DisplaySetAndViewportOptions
}
type GlobalProtocolOptions = DisplaySetAndViewportOptions
type SetProtocolOptions =
ViewportSpecificProtocolOptions | GlobalProtocolOptions;
type HangingProtocolMatchDetails = {
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
viewportMatchDetails: ViewportMatchDetails[];
hpAlreadyApplied: boolean[];
viewportMatchDetails: Map<number, ViewportMatchDetails>;
hpAlreadyApplied: Map<number, boolean>;
};
type MatchingRule = {
@@ -130,15 +148,12 @@ type Protocol = {
imageLoadStrategy?: string; // Todo: this should be types specifically
protocolMatchingRules?: MatchingRule[];
numberOfPriorsReferenced?: number;
syncDataForViewports?: boolean;
};
type ProtocolGenerator = ({servicesManager: any, commandsManager: any}) => {
protocol: Protocol;
matchingDisplaySets: any;
};
export type {
ProtocolGenerator,
SetProtocolOptions,
ViewportOptions,
ViewportMatchDetails,
DisplaySetMatchDetails,
@@ -153,5 +168,9 @@ export type {
MatchingRule,
SyncGroup,
initialImageOptions,
DisplaySetInfo
DisplaySetInfo,
GlobalProtocolOptions,
ViewportSpecificProtocolOptions,
DisplaySetAndViewportOptions
};
+5
View File
@@ -20,10 +20,12 @@ import progressTrackingUtils from './progressTrackingUtils';
import isLowPriorityModality from './isLowPriorityModality';
import { isImage } from './isImage';
import isDisplaySetReconstructable from './isDisplaySetReconstructable';
import sortInstancesByPosition from './sortInstancesByPosition';
import imageIdToURI from './imageIdToURI';
import debounce from './debounce';
import roundNumber from './roundNumber';
import downloadCSVReport from './downloadCSVReport';
import isEqualWithin from './isEqualWithin';
// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
@@ -46,6 +48,7 @@ const utils = {
hotkeys,
Queue,
isDicomUid,
isEqualWithin,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
@@ -72,12 +75,14 @@ export {
hotkeys,
Queue,
isDicomUid,
isEqualWithin,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
isLowPriorityModality,
isImage,
isDisplaySetReconstructable,
sortInstancesByPosition,
imageIdToURI,
debounce,
roundNumber,
+1
View File
@@ -17,6 +17,7 @@ describe('Top level exports', () => {
'b64toBlob',
'formatDate',
'formatPN',
'isEqualWithin',
//'loadAndCacheDerivedDisplaySets',
'isDisplaySetReconstructable',
'isImage',
@@ -1,4 +1,5 @@
import toNumber from './toNumber';
import sortInstancesByPosition from './sortInstancesByPosition';
// TODO: Is 10% a reasonable spacingTolerance for spacing?
const spacingTolerance = 0.2;
@@ -28,10 +29,12 @@ export default function isDisplaySetReconstructable(instances) {
return { value: false };
}
const sortedInstances = sortInstancesByPosition(instances);
if (isMultiframe) {
return processMultiframe(instances[0]);
return processMultiframe(sortedInstances[0]);
} else {
return processSingleframe(instances);
return processSingleframe(sortedInstances);
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* returns equal if the two arrays are identical within the
* given tolerance.
*
* @param v1 - The first array of values
* @param v2 - The second array of values.
* @param tolerance - The acceptable tolerance, the default is 0.00001
*
* @returns True if the two values are within the tolerance levels.
*/
export default function isEqualWithin(
v1: number[] | Float32Array,
v2: number[] | Float32Array,
tolerance = 1e-5
): boolean {
if (v1.length !== v2.length) {
return false;
}
for (let i = 0; i < v1.length; i++) {
if (Math.abs(v1[i] - v2[i]) > tolerance) {
return false;
}
}
return true;
}
@@ -0,0 +1,61 @@
import sortInstances from './sortInstancesByPosition';
describe('sortInstances', () => {
it('should sort instances based on their imagePositionPatient', () => {
const instances = [
{
ImagePositionPatient: [0, 0, 2],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 1],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 0],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([
{
ImagePositionPatient: [0, 0, 0],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 1],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 2],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
]);
});
it('should return the same instances if there is only one instance', () => {
const instances = [
{
ImagePositionPatient: [0, 0, 0],
},
];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([
{
ImagePositionPatient: [0, 0, 0],
},
]);
});
it('should return the same instances if there are no instances', () => {
const instances = [];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([]);
});
});
@@ -0,0 +1,67 @@
import { vec3 } from 'gl-matrix';
/**
* Given an array of imageIds, sort them based on their imagePositionPatient, and
* also returns the spacing between images and the origin of the reference image
*
* @param imageIds - array of imageIds
* @param scanAxisNormal - [x, y, z] array or gl-matrix vec3
*
* @returns The sortedImageIds, zSpacing, and origin of the first image in the series.
*/
export default function sortInstances(instances: Array<any>) {
// Return if only one instance e.g., multiframe
if (instances.length <= 1) {
return instances;
}
const {
ImagePositionPatient: referenceImagePositionPatient,
ImageOrientationPatient,
} = instances[Math.floor(instances.length / 2)]; // this prevents getting scout image as test image
const rowCosineVec = vec3.fromValues(
ImageOrientationPatient[0],
ImageOrientationPatient[1],
ImageOrientationPatient[2]
);
const colCosineVec = vec3.fromValues(
ImageOrientationPatient[3],
ImageOrientationPatient[4],
ImageOrientationPatient[5]
);
const scanAxisNormal = vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
const refIppVec = vec3.set(
vec3.create(),
referenceImagePositionPatient[0],
referenceImagePositionPatient[1],
referenceImagePositionPatient[2]
);
const distanceInstancePairs = instances.map(instance => {
const imagePositionPatient = instance.ImagePositionPatient;
const positionVector = vec3.create();
vec3.sub(
positionVector,
referenceImagePositionPatient,
imagePositionPatient
);
const distance = vec3.dot(positionVector, scanAxisNormal);
return {
distance,
instance,
};
});
distanceInstancePairs.sort((a, b) => b.distance - a.distance);
const sortedInstances = distanceInstancePairs.map(a => a.instance);
return sortedInstances;
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Given the flatten index, and rows and column, it returns the
* row and column index
*/
const unravelIndex = (index, numRows, numCols) => {
const row = Math.floor(index / numCols);
const col = index % numCols;
return { row, col };
};
export default unravelIndex;
+13
View File
@@ -0,0 +1,13 @@
// prettier-ignore
// @ts-nocheck
/**
* Generates a unique id that has limited chance of collision
*
* @see {@link https://stackoverflow.com/a/2117523/1867984|StackOverflow: Source}
* @returns a v4 compliant GUID
*/
export default function uuidv4(): string {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}