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:
1 parent
08c58b0634
commit
83ea228d86
254 files changed
+12856
-3141
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -33,7 +33,6 @@ describe('Top level exports', () => {
|
||||
'ToolBarService',
|
||||
'Types',
|
||||
'ViewportGridService',
|
||||
'SegmentationService',
|
||||
'HangingProtocolService',
|
||||
'UserAuthenticationService',
|
||||
'IWebApiDataSource',
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user