feat: cornerstone3D stack and volume viewports (#2787)
* feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock * feat: overlay component (#21) * fix default displayset options * add viewport overlay * apply review comments * feat: loading and orientation indicators (#22) * add loading indicator * add orientation marker initial work * apply review comments * fix: orientation markers (#23) * finished the orientation markers * fix various broken cypress tests * apply review comments * update yarn lock * feat: re-working measurement tracking mode with cornerstone3d (#2805) * feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: Add Measurement tracking mode with cs3D (#2789) * feat: first render for cornerstone3d tracked viewport * make tool active work * wip for SR extension * renamed dicom sr to cornerstone dicom sr * remove cornerstone from dicom pdf and video * move dicom sr logic to sr extension * feat: Add hydration for the length tool * fix SR display tool for length using cs3d * fix default config * fix: various bugs with sr viewport and tracking * fix promptying to continue tracking for when SR is created * feat: add keep trackign of unique identifiers * fix hydration for same imageIds * feat: Add SR toolGroup creation on modeEnter * feat: remove the need for separate mapper for SR hydration * add SR display for ellipse * handle hydration of elliptical ROI tool * remove cornerstone extension * add arrow mapping * feat: Add ArrowAnnotate SR display and hydration * apply review comments * apply review comments * move viewport labels to the viewportData * apply review comments * fix: integration cypress tests with cornerstone3D and add CINE tool (#2795) * fix: integration cypress tests with cornerstone3D * revert to addOrUpdate as it makes more sense * fix local drag and drop * fix tests * move dicomLoaderService to cornerstone extension * fix various import bugs * fix bug for local PT series * fix various unit tests * bump cs3d versions * add angle and magnify tool * bump dependencies to avoid broken peerDeps * feat: add initial work for capture using cs3d * feat: show annotations on the image capture * feat: add svg layer export * feat: Add CINE Tool * feat: remove unnecessary viewport rendering for cine state changes Co-authored-by: Erik Ziegler <erik.sweed@gmail.com> * docs: modify and improve documentation (#2800) * cleanup docs versionings * feat: Add all version explanations * version docs for 3.0 * wip for changing docs * wip for updated docs * add utility module documentation * fix demo with nohoisting of history * add slides and video to resources * apply review comments * fix: drag and drop SR into SR viewport (#2803) * update yarn lock Co-authored-by: Erik <erik.sweed@gmail.com> * update pathnames to match v3-stable * update the e2e pathname * fix: various bugs with regard to tracking workflow (#2811) * fix: various issues with measurement panel * fix: update default tool style for annotations * fix: annotatoin label getting removed * feat: Add backward compatibility for SR hydration with legacy cornerstone * fix: cursors and ellipse ROI max style * fix: ArrowAnnotate SRDisplay * apply review comments * bump package versions * fix: bugin rehydration of SR * fix: e2e tests * fix active viewport thickness and arrowTool ui * add readme for measurement tracking * use uploaded image for readme * add back images * try to fix e2e test * fix: window level presets hotkeys * Update README.md * update yarn lock * feat: volume api and TMTV mode (#2817) * feat: volumeAPI and TMTV mode * feat: use cs3d cache service to obtain viewportData * wip for volume api * wip: fusion viewport * feat: add blend mode option * wip for image scrollbar * fix drag and drop thumbnail * wip for image scrollbar for voluems * fix: element mismatch bug for scroll * feat: Add image scrollbar to volumes * feat: add syncGroups to volume api * feat: add tmtv mode initial setup * feat: add initial image options for the stack viewports * feat: Add custom load strategy for volume viewports via HP * apply review comments fix: Jump presets cs3d (#2812) * feat: Add JumpPreset to OHIF for Cornerstone3D * fix: accessing viewport service from servicesManager feat: volume API and TMTV mode (#2814) * fix: do not display overlays on mip viewports * feat: add optional disableCommands for toggle buttons * fix: toggleCrossharis * feat: config the crosshairs * feat: add PetSUV Panel for changing metadata * feat: initial work for the rectangleROIThreshold panel * wip: measurement service * roi threshold working * feat: Add displayText to segmentations * feat: add remove segmentation * feat: add csv export * feat: add RT export for annotations in tmtv mode * fix: fusion to use pt in tmtv mode and measuremet mappings * fix: various bugs * apply review comments * apply review comments * feat: add fusion color maps * add readme to tmtv mode * Update README.md * fix: try to fix build * fix unit tests * Update README.md * feat: add about to the panel * fix: changing strategy in roi panel * apply review comments * update package versions * wip for stackPrefetch * fix: cornerstone3d hydration and renaming (#2818) * update readme * renamed cornerstone extension * wip for renaming cornerstone3D variables * wip for renaming cornerstone3D variables * wip for fixing bugs for SR viewport * fix: jumpToMeasurement and initial label after hydration * fix: fileName capitalization * feat: use the new prefetch stack in the cs3d (#2820) * feat: use the new prefetch stack in the cs3d * use viewport scroll api for stack viewport * fix cine stop when scrollbar changes * feat: use new prefetch events * fix loading state to not show repeatedly * fix: various bugs for tmtv mode thresholding and new icons (#2823) * feat: make tmtv mode available in worklist * feat: add new icons for tmtv mode * feat: add fusion color icon * fix: parallel scale calculation * fix: bump Cornerstone to get large image support working * fix: Fix issues with CPU viewport flipping, including config files * fix: bump cornerstone to fix magnify tool * fix: Bump Cornerstone version to fix resetCamera issue in StackViewport * ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys * fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors * feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838) Also works with the compile time add that the existing cli uses, so that both build types work. Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes. * bump: dependency versions to fix hydration bugs (#2848) * bump: dcmjs version to fix hydration bugs * try to fix tests * bump dependency versions Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
668 files changed
+41669
-13410
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools';
|
||||
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
|
||||
const EVENTS = {
|
||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||
};
|
||||
|
||||
export type SyncGroup = {
|
||||
type: string;
|
||||
id: string;
|
||||
source: boolean;
|
||||
target: boolean;
|
||||
};
|
||||
|
||||
const POSITION = 'cameraposition';
|
||||
const VOI = 'voi';
|
||||
|
||||
export default class SyncGroupService {
|
||||
serviceManager: any;
|
||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor(serviceManager) {
|
||||
this.serviceManager = serviceManager;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
//
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
private _createSynchronizer(type: string, id: string) {
|
||||
type = type.toLowerCase();
|
||||
if (type === POSITION) {
|
||||
return synchronizers.createCameraPositionSynchronizer(id);
|
||||
} else if (type === VOI) {
|
||||
return synchronizers.createVOISynchronizer(id);
|
||||
}
|
||||
}
|
||||
|
||||
public addViewportToSyncGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string,
|
||||
syncGroups?: SyncGroup[]
|
||||
): void {
|
||||
if (!syncGroups || !syncGroups.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncGroups.forEach(syncGroup => {
|
||||
const { type, id, target, source } = syncGroup;
|
||||
|
||||
let synchronizer = SynchronizerManager.getSynchronizer(id);
|
||||
|
||||
if (!synchronizer) {
|
||||
synchronizer = this._createSynchronizer(type, id);
|
||||
}
|
||||
|
||||
if (target && source) {
|
||||
synchronizer.add({
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
});
|
||||
return;
|
||||
} else if (source) {
|
||||
synchronizer.addSource({
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
});
|
||||
} else if (target) {
|
||||
synchronizer.addTarget({
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
SynchronizerManager.destroy();
|
||||
}
|
||||
|
||||
public removeViewportFromSyncGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string
|
||||
): void {
|
||||
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
||||
|
||||
synchronizers.forEach(synchronizer => {
|
||||
if (!synchronizer) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronizer.remove({
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
});
|
||||
|
||||
// check if any viewport is left in any of the sync groups, if not, delete that sync group
|
||||
const sourceViewports = synchronizer.getSourceViewports();
|
||||
const targetViewports = synchronizer.getTargetViewports();
|
||||
|
||||
if (!sourceViewports.length && !targetViewports.length) {
|
||||
SynchronizerManager.destroySynchronizer(synchronizer.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import SyncGroupService from './SyncGroupService';
|
||||
|
||||
export default function ExtendedSyncGroupService(serviceManager) {
|
||||
return {
|
||||
name: 'SyncGroupService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new SyncGroupService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
||||
};
|
||||
|
||||
type Tool = {
|
||||
toolName: string;
|
||||
bindings?: typeof Enums.MouseBindings | Enums.KeyboardBindings;
|
||||
};
|
||||
|
||||
type Tools = {
|
||||
active: Tool[];
|
||||
passive?: Tool[];
|
||||
enabled?: Tool[];
|
||||
disabled?: Tool[];
|
||||
};
|
||||
|
||||
export default class ToolGroupService {
|
||||
serviceManager: any;
|
||||
private toolGroupIds: Set<string> = new Set();
|
||||
/**
|
||||
* Service-specific
|
||||
*/
|
||||
listeners: { [key: string]: Function[] };
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor(serviceManager) {
|
||||
const { CornerstoneViewportService } = serviceManager.services;
|
||||
this.CornerstoneViewportService = CornerstoneViewportService;
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cornerstone ToolGroup for a given toolGroup UID
|
||||
* @param {string} toolGroupId - The toolGroup uid
|
||||
* @returns {IToolGroup} - The toolGroup
|
||||
*/
|
||||
public getToolGroup(toolGroupId: string): Types.IToolGroup | void {
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
return toolGroup;
|
||||
}
|
||||
|
||||
public getToolGroupIds(): string[] {
|
||||
return Array.from(this.toolGroupIds);
|
||||
}
|
||||
|
||||
public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void {
|
||||
const renderingEngine = this.CornerstoneViewportService.getRenderingEngine();
|
||||
return ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngine.id
|
||||
);
|
||||
}
|
||||
|
||||
public getActiveToolForViewport(viewportId: string): string {
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId);
|
||||
if (!toolGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toolGroup.getActivePrimaryMouseButtonTool();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
ToolGroupManager.destroy();
|
||||
this.toolGroupIds = new Set();
|
||||
}
|
||||
|
||||
public disable(viewportId: string, renderingEngineId: string): void {
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroup.removeViewports(renderingEngineId, viewportId);
|
||||
|
||||
const viewportIds = toolGroup.getViewportIds();
|
||||
// if (viewportIds.length === 0) {
|
||||
// ToolGroupManager.destroyToolGroup(toolGroup.id);
|
||||
// }
|
||||
}
|
||||
|
||||
public addViewportToToolGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string,
|
||||
toolGroupId?: string
|
||||
): void {
|
||||
if (!toolGroupId) {
|
||||
// If toolGroupId is not provided, add the viewport to all toolGroups
|
||||
const toolGroups = ToolGroupManager.getAllToolGroups();
|
||||
toolGroups.forEach(toolGroup => {
|
||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||
});
|
||||
} else {
|
||||
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
if (!toolGroup) {
|
||||
toolGroup = this.createToolGroup(toolGroupId);
|
||||
}
|
||||
|
||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||
}
|
||||
|
||||
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
|
||||
}
|
||||
|
||||
public createToolGroup(toolGroupId: string): Types.IToolGroup {
|
||||
if (this.getToolGroup(toolGroupId)) {
|
||||
throw new Error(`ToolGroup ${toolGroupId} already exists`);
|
||||
}
|
||||
|
||||
// if the toolGroup doesn't exist, create it
|
||||
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
||||
this.toolGroupIds.add(toolGroupId);
|
||||
|
||||
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
|
||||
|
||||
return toolGroup;
|
||||
}
|
||||
|
||||
public addToolsToToolGroup(
|
||||
toolGroupId: string,
|
||||
tools: Array<Tool>,
|
||||
configs: any = {}
|
||||
): void {
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
// this.changeConfigurationIfNecessary(toolGroup, volumeId);
|
||||
this._addTools(toolGroup, tools, configs);
|
||||
this._setToolsMode(toolGroup, tools);
|
||||
}
|
||||
|
||||
public createToolGroupAndAddTools(
|
||||
toolGroupId: string,
|
||||
tools: Array<Tool>,
|
||||
configs: any = {}
|
||||
): Types.IToolGroup {
|
||||
const toolGroup = this.createToolGroup(toolGroupId);
|
||||
this.addToolsToToolGroup(toolGroupId, tools, configs);
|
||||
return toolGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
|
||||
// handle specific assignment for volumeUID (e.g., fusion)
|
||||
const toolInstances = toolGroup._toolInstances;
|
||||
// Object.values(toolInstances).forEach(toolInstance => {
|
||||
// if (toolInstance.configuration) {
|
||||
// toolInstance.configuration.volumeUID = volumeUID;
|
||||
// }
|
||||
// });
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the tool's configuration based on the tool name and tool group id
|
||||
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
|
||||
* @param toolName - The name of the tool
|
||||
* @returns The configuration of the tool.
|
||||
*/
|
||||
public getToolConfiguration(toolGroupId: string, toolName: string) {
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
if (!toolGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tool = toolGroup.getToolInstance(toolName);
|
||||
if (!tool) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tool.configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tool instance configuration. This will update the tool instance configuration
|
||||
* on the toolGroup
|
||||
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
|
||||
* @param toolName - The name of the tool
|
||||
* @param config - The configuration object that you want to set.
|
||||
*/
|
||||
public setToolConfiguration(toolGroupId, toolName, config) {
|
||||
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||
const toolInstance = toolGroup.getToolInstance(toolName);
|
||||
toolInstance.configuration = config;
|
||||
}
|
||||
|
||||
private _getToolNames(toolGroupTools: Tools): string[] {
|
||||
const toolNames = [];
|
||||
toolGroupTools.active.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
if (toolGroupTools.passive) {
|
||||
toolGroupTools.passive.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
}
|
||||
|
||||
if (toolGroupTools.enabled) {
|
||||
toolGroupTools.enabled.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
}
|
||||
|
||||
if (toolGroupTools.disabled) {
|
||||
toolGroupTools.disabled.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
}
|
||||
|
||||
return toolNames;
|
||||
}
|
||||
|
||||
private _setToolsMode(toolGroup, tools) {
|
||||
const { active, passive, enabled, disabled } = tools;
|
||||
active.forEach(({ toolName, bindings }) => {
|
||||
toolGroup.setToolActive(toolName, { bindings });
|
||||
});
|
||||
|
||||
if (passive) {
|
||||
passive.forEach(({ toolName }) => {
|
||||
toolGroup.setToolPassive(toolName);
|
||||
});
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
enabled.forEach(({ toolName }) => {
|
||||
toolGroup.setToolEnabled(toolName);
|
||||
});
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
disabled.forEach(({ toolName }) => {
|
||||
toolGroup.setToolDisabled(toolName);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _addTools(toolGroup, tools, configs) {
|
||||
const toolNames = this._getToolNames(tools);
|
||||
toolNames.forEach(toolName => {
|
||||
// Initialize the toolConfig if no configuration is provided
|
||||
const toolConfig = configs[toolName] ?? {};
|
||||
|
||||
// if (volumeUID) {
|
||||
// toolConfig.volumeUID = volumeUID;
|
||||
// }
|
||||
|
||||
toolGroup.addTool(toolName, { ...toolConfig });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import ToolGroupService from './ToolGroupService';
|
||||
|
||||
export default function ExtendedToolGroupService(serviceManager) {
|
||||
return {
|
||||
name: 'ToolGroupService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new ToolGroupService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
import {
|
||||
cache as cs3DCache,
|
||||
Enums,
|
||||
Types,
|
||||
volumeLoader,
|
||||
} from '@cornerstonejs/core';
|
||||
import { utils, pubSubServiceInterface } from '@ohif/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
|
||||
export type StackData = {
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
imageIds: string[];
|
||||
frameRate?: number;
|
||||
isClip?: boolean;
|
||||
initialImageIndex?: number | string | null;
|
||||
viewportType: Enums.ViewportType;
|
||||
};
|
||||
|
||||
export type VolumeData = {
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
|
||||
imageIds: string[][]; // can have more than one imageId list (fusion)
|
||||
volumes: Types.IVolume[];
|
||||
viewportType: Enums.ViewportType;
|
||||
};
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'streaming-wadors';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
|
||||
};
|
||||
|
||||
class CornerstoneCacheService {
|
||||
stackImageIds: Map<string, string[]> = new Map();
|
||||
volumeImageIds: Map<string, string[]> = new Map();
|
||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
public getCacheSize() {
|
||||
return cs3DCache.getCacheSize();
|
||||
}
|
||||
|
||||
public getCacheFreeSpace() {
|
||||
return cs3DCache.getBytesAvailable();
|
||||
}
|
||||
|
||||
public async getViewportData(
|
||||
viewportIndex: number,
|
||||
displaySets: unknown[],
|
||||
viewportType: string,
|
||||
dataSource: unknown,
|
||||
initialImageIndex?: number
|
||||
): Promise<StackData | VolumeData> {
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
||||
let viewportData: StackData | VolumeData;
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||
viewportData = await this._getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
public async invalidateViewportData(
|
||||
viewportData: VolumeData,
|
||||
invalidatedDisplaySetInstanceUID: string,
|
||||
dataSource,
|
||||
DisplaySetService
|
||||
) {
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
||||
}
|
||||
|
||||
const volumeId = invalidatedDisplaySetInstanceUID;
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (volume) {
|
||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||
}
|
||||
|
||||
const displaySets = viewportData.displaySetInstanceUIDs.map(
|
||||
DisplaySetService.getDisplaySetByUID
|
||||
);
|
||||
|
||||
const newViewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
);
|
||||
|
||||
return newViewportData;
|
||||
}
|
||||
|
||||
private _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
): StackData {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
let stackImageIds = this.stackImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!stackImageIds) {
|
||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||
|
||||
const stackData: StackData = {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
viewportType: Enums.ViewportType.STACK,
|
||||
imageIds: stackImageIds,
|
||||
};
|
||||
|
||||
if (typeof initialImageIndex === 'number') {
|
||||
stackData.initialImageIndex = initialImageIndex;
|
||||
}
|
||||
|
||||
return stackData;
|
||||
}
|
||||
|
||||
private async _getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
): Promise<VolumeData> {
|
||||
// Check the cache for multiple scenarios to see if we need to
|
||||
// decache the volume data from other viewports or not
|
||||
|
||||
const volumeImageIdsArray = [];
|
||||
const volumes = [];
|
||||
|
||||
for (const displaySet of displaySets) {
|
||||
const volumeId = displaySet.displaySetInstanceUID;
|
||||
|
||||
let volumeImageIds = this.volumeImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
let volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volumeImageIds || !volume) {
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
imageIds: volumeImageIds,
|
||||
});
|
||||
|
||||
this.volumeImageIds.set(
|
||||
displaySet.displaySetInstanceUID,
|
||||
volumeImageIds
|
||||
);
|
||||
}
|
||||
|
||||
volumeImageIdsArray.push(volumeImageIds);
|
||||
volumes.push(volume);
|
||||
}
|
||||
|
||||
// assert displaySets are from the same study
|
||||
const { StudyInstanceUID } = displaySets[0];
|
||||
const displaySetInstanceUIDs = [];
|
||||
|
||||
displaySets.forEach(displaySet => {
|
||||
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
|
||||
throw new Error('Display sets are not from the same study');
|
||||
}
|
||||
|
||||
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
|
||||
});
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUIDs,
|
||||
imageIds: volumeImageIdsArray,
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
volumes,
|
||||
};
|
||||
}
|
||||
|
||||
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
}
|
||||
|
||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
return stackImageIds.map(imageId => {
|
||||
const imageURI = utils.imageIdToURI(imageId);
|
||||
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CacheService = new CornerstoneCacheService();
|
||||
export default CacheService;
|
||||
@@ -0,0 +1,579 @@
|
||||
import { pubSubServiceInterface } from '@ohif/core';
|
||||
import {
|
||||
RenderingEngine,
|
||||
StackViewport,
|
||||
Enums,
|
||||
Types,
|
||||
getRenderingEngine,
|
||||
utilities as csUtils,
|
||||
VolumeViewport,
|
||||
cache,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||
import { IViewportService } from './IViewportService';
|
||||
import { RENDERING_ENGINE_ID } from './constants';
|
||||
import ViewportInfo, {
|
||||
ViewportOptions,
|
||||
DisplaySetOptions,
|
||||
PublicViewportOptions,
|
||||
} from './Viewport';
|
||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||
import {
|
||||
setColormap,
|
||||
setLowerUpperColorTransferFunction,
|
||||
} from '../../utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_INFO_CREATED:
|
||||
'event::cornerstone::viewportservice:viewportinfocreated',
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||
* updating the viewport.
|
||||
*/
|
||||
class CornerstoneViewportService implements IViewportService {
|
||||
renderingEngine: Types.IRenderingEngine | null;
|
||||
viewportsInfo: Map<number, ViewportInfo>;
|
||||
viewportGridResizeObserver: ResizeObserver | null;
|
||||
|
||||
/**
|
||||
* Service-specific
|
||||
*/
|
||||
EVENTS: { [key: string]: string };
|
||||
listeners: { [key: string]: Array<(...args: any[]) => void> };
|
||||
_broadcastEvent: unknown; // we should be able to extend the PubSub class to get this
|
||||
// Some configs
|
||||
enableResizeDetector: true;
|
||||
resizeRefreshRateMs: 200;
|
||||
resizeRefreshMode: 'debounce';
|
||||
|
||||
constructor(servicesManager) {
|
||||
this.renderingEngine = null;
|
||||
this.viewportGridResizeObserver = null;
|
||||
this.viewportsInfo = new Map();
|
||||
//
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
const { HangingProtocolService } = servicesManager.services;
|
||||
this.HangingProtocolService = HangingProtocolService;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the HTML element to the viewportService
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} elementRef
|
||||
*/
|
||||
public enableElement(
|
||||
viewportIndex: number,
|
||||
viewportOptions: PublicViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
) {
|
||||
const viewportId =
|
||||
viewportOptions.viewportId || this.getViewportId(viewportIndex);
|
||||
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
|
||||
viewportInfo.setElement(elementRef);
|
||||
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||
}
|
||||
|
||||
public getViewportId(viewportIndex: number): string {
|
||||
return `viewport-${viewportIndex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* It retrieves the renderingEngine if it does exist, or creates one otherwise
|
||||
* @returns {RenderingEngine} rendering engine
|
||||
*/
|
||||
public getRenderingEngine() {
|
||||
// get renderingEngine from cache if it exists
|
||||
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||
|
||||
if (renderingEngine) {
|
||||
this.renderingEngine = renderingEngine;
|
||||
return this.renderingEngine;
|
||||
}
|
||||
|
||||
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
|
||||
this.renderingEngine = new RenderingEngine(RENDERING_ENGINE_ID);
|
||||
}
|
||||
|
||||
return this.renderingEngine;
|
||||
}
|
||||
|
||||
/**
|
||||
* It triggers the resize on the rendering engine.
|
||||
*/
|
||||
public resize() {
|
||||
const immediate = true;
|
||||
const resetPan = false;
|
||||
const resetZoom = false;
|
||||
|
||||
this.renderingEngine.resize(immediate, resetPan, resetZoom);
|
||||
this.renderingEngine.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the viewport from cornerstone, and destroys the rendering engine
|
||||
*/
|
||||
public destroy() {
|
||||
this._removeResizeObserver();
|
||||
this.viewportGridResizeObserver = null;
|
||||
this.renderingEngine.destroy();
|
||||
this.renderingEngine = null;
|
||||
cache.purgeVolumeCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the viewport inside the renderingEngine, if no viewport is left
|
||||
* it destroys the renderingEngine.
|
||||
* @param viewportIndex
|
||||
*/
|
||||
public disableElement(viewportIndex: number) {
|
||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
this.renderingEngine.disableElement(viewportId);
|
||||
this.viewportsInfo.delete(viewportIndex);
|
||||
|
||||
if (this.viewportsInfo.size === 0) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the renderingEngine to enable the element for the given viewport index
|
||||
* and sets the displaySet data to the viewport
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} displaySet
|
||||
* @param {*} dataSource
|
||||
* @returns
|
||||
*/
|
||||
public setViewportDisplaySets(
|
||||
viewportIndex: number,
|
||||
viewportData: StackData | VolumeData,
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
publicDisplaySetOptions: DisplaySetOptions[]
|
||||
): void {
|
||||
const renderingEngine = this.getRenderingEngine();
|
||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||
|
||||
const {
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
} = this._getViewportAndDisplaySetOptions(
|
||||
publicViewportOptions,
|
||||
publicDisplaySetOptions,
|
||||
viewportInfo
|
||||
);
|
||||
|
||||
viewportInfo.setViewportOptions(viewportOptions);
|
||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||
|
||||
this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const element = viewportInfo.getElement();
|
||||
const type = viewportInfo.getViewportType();
|
||||
const background = viewportInfo.getBackground();
|
||||
const orientation = viewportInfo.getOrientation();
|
||||
|
||||
const viewportInput: Types.PublicViewportInput = {
|
||||
viewportId,
|
||||
element,
|
||||
type,
|
||||
defaultOptions: {
|
||||
background,
|
||||
orientation,
|
||||
},
|
||||
};
|
||||
|
||||
// Todo: this is not optimal at all, we are re-enabling the already enabled
|
||||
// element which is not what we want. But enabledElement as part of the
|
||||
// renderingEngine is designed to be used like this. This will trigger
|
||||
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
this._setDisplaySets(viewportId, viewportData, viewportInfo);
|
||||
}
|
||||
|
||||
public getCornerstoneViewport(
|
||||
viewportId: string
|
||||
): Types.IStackViewport | Types.IVolumeViewport | null {
|
||||
const viewportInfo = this.getViewportInfo(viewportId);
|
||||
|
||||
if (
|
||||
!viewportInfo ||
|
||||
!this.renderingEngine ||
|
||||
this.renderingEngine.hasBeenDestroyed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewport = this.renderingEngine.getViewport(viewportId);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
public getCornerstoneViewportByIndex(
|
||||
viewportIndex: number
|
||||
): Types.IStackViewport | Types.IVolumeViewport | null {
|
||||
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
||||
|
||||
if (
|
||||
!viewportInfo ||
|
||||
!this.renderingEngine ||
|
||||
this.renderingEngine.hasBeenDestroyed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewport = this.renderingEngine.getViewport(
|
||||
viewportInfo.getViewportId()
|
||||
);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the viewportIndex for the provided viewportId
|
||||
* @param {string} viewportId - the viewportId
|
||||
* @returns {number} - the viewportIndex
|
||||
*/
|
||||
public getViewportInfoByIndex(viewportIndex: number): ViewportInfo {
|
||||
return this.viewportsInfo.get(viewportIndex);
|
||||
}
|
||||
|
||||
public getViewportInfo(viewportId: string): ViewportInfo {
|
||||
// @ts-ignore
|
||||
for (const [index, viewport] of this.viewportsInfo.entries()) {
|
||||
if (viewport.getViewportId() === viewportId) {
|
||||
return viewport;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_setStackViewport(
|
||||
viewport: Types.IStackViewport,
|
||||
viewportData: StackData,
|
||||
viewportInfo: ViewportInfo
|
||||
) {
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
|
||||
const { imageIds, initialImageIndex } = viewportData;
|
||||
|
||||
let initialImageIndexToUse = initialImageIndex;
|
||||
|
||||
if (!initialImageIndexToUse) {
|
||||
initialImageIndexToUse =
|
||||
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
||||
}
|
||||
|
||||
const { voi, voiInverted } = displaySetOptions[0];
|
||||
const properties = {};
|
||||
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
voi.windowWidth,
|
||||
voi.windowCenter
|
||||
);
|
||||
properties.voiRange = { lower, upper };
|
||||
}
|
||||
|
||||
if (voiInverted !== undefined) {
|
||||
properties.invert = voiInverted;
|
||||
}
|
||||
|
||||
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||
viewport.setProperties(properties);
|
||||
});
|
||||
}
|
||||
|
||||
private _getInitialImageIndexForStackViewport(
|
||||
viewportInfo: ViewportInfo,
|
||||
imageIds?: string[]
|
||||
): number {
|
||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||
|
||||
if (!initialImageOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { index, preset } = initialImageOptions;
|
||||
return this._getInitialImageIndex(imageIds.length, index, preset);
|
||||
}
|
||||
|
||||
_getInitialImageIndex(
|
||||
numberOfSlices: number,
|
||||
imageIndex?: number,
|
||||
preset?: JumpPresets
|
||||
): number {
|
||||
const lastSliceIndex = numberOfSlices - 1;
|
||||
|
||||
if (imageIndex !== undefined) {
|
||||
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
|
||||
}
|
||||
|
||||
if (preset === JumpPresets.First) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (preset === JumpPresets.Last) {
|
||||
return lastSliceIndex;
|
||||
}
|
||||
|
||||
if (preset === JumpPresets.Middle) {
|
||||
return Math.floor(lastSliceIndex / 2);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async _setVolumeViewport(
|
||||
viewport: Types.IVolumeViewport,
|
||||
viewportData: VolumeData,
|
||||
viewportInfo: ViewportInfo
|
||||
): Promise<void> {
|
||||
// TODO: We need to overhaul the way data sources work so requests can be made
|
||||
// async. I think we should follow the image loader pattern which is async and
|
||||
// has a cache behind it.
|
||||
// The problem is that to set this volume, we need the metadata, but the request is
|
||||
// already in-flight, and the promise is not cached, so we have no way to wait for
|
||||
// it and know when it has fully arrived.
|
||||
// loadStudyMetadata(StudyInstanceUID) => Promise([instances for study])
|
||||
// loadSeriesMetadata(StudyInstanceUID, SeriesInstanceUID) => Promise([instances for series])
|
||||
// If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire
|
||||
// a request through the data source?
|
||||
// (This call may or may not create sub-requests for series metadata)
|
||||
const volumeInputArray = [];
|
||||
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
||||
const { HangingProtocolService } = this;
|
||||
|
||||
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
||||
const imageIds = viewportData.imageIds[i];
|
||||
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
|
||||
const displaySetOptions = displaySetOptionsArray[i];
|
||||
|
||||
const volumeId = displaySetInstanceUID;
|
||||
|
||||
// if (displaySet.needsRerendering) {
|
||||
// console.warn('Removing volume from cache', volumeId);
|
||||
// cache.removeVolumeLoadObject(volumeId);
|
||||
// displaySet.needsRerendering = false;
|
||||
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
|
||||
// }
|
||||
|
||||
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
||||
|
||||
const callback = ({ volumeActor }) => {
|
||||
voiCallbacks.forEach(callback => callback(volumeActor));
|
||||
};
|
||||
|
||||
volumeInputArray.push({
|
||||
imageIds,
|
||||
volumeId,
|
||||
callback,
|
||||
blendMode: displaySetOptions.blendMode,
|
||||
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||
!HangingProtocolService.customImageLoadPerformed
|
||||
) {
|
||||
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
||||
return HangingProtocolService.runImageLoadStrategy({
|
||||
viewportId: viewport.id,
|
||||
volumeInputArray,
|
||||
});
|
||||
}
|
||||
|
||||
viewportData.volumes.forEach(volume => {
|
||||
volume.load();
|
||||
});
|
||||
|
||||
this.setVolumesForViewport(viewport, volumeInputArray);
|
||||
}
|
||||
|
||||
public setVolumesForViewport(viewport, volumeInputArray) {
|
||||
viewport.setVolumes(volumeInputArray).then(() => {
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||
|
||||
if (
|
||||
initialImageOptions &&
|
||||
(initialImageOptions.preset !== undefined ||
|
||||
initialImageOptions.index !== undefined)
|
||||
) {
|
||||
const { index, preset } = initialImageOptions;
|
||||
|
||||
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
||||
viewport
|
||||
);
|
||||
|
||||
const imageIndex = this._getInitialImageIndex(
|
||||
numberOfSlices,
|
||||
index,
|
||||
preset
|
||||
);
|
||||
|
||||
csToolsUtils.jumpToSlice(viewport.element, {
|
||||
imageIndex,
|
||||
});
|
||||
}
|
||||
|
||||
viewport.render();
|
||||
});
|
||||
}
|
||||
|
||||
public updateViewport(viewportIndex, viewportData) {
|
||||
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const viewport = this.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (viewport instanceof VolumeViewport) {
|
||||
this._setVolumeViewport(viewport, viewportData, viewportInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
this._setStackViewport(viewport, viewportData, viewportInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_getVOICallbacks(volumeId, displaySetOptions) {
|
||||
const { voi, voiInverted: inverted, colormap } = displaySetOptions;
|
||||
|
||||
const voiCallbackArray = [];
|
||||
|
||||
// If colormap is set, use it to set the color transfer function
|
||||
if (colormap) {
|
||||
voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap));
|
||||
}
|
||||
|
||||
if (voi instanceof Object && voi.windowWidth && voi.windowCenter) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
windowWidth,
|
||||
windowCenter
|
||||
);
|
||||
voiCallbackArray.push(volumeActor =>
|
||||
setLowerUpperColorTransferFunction({
|
||||
volumeActor,
|
||||
lower,
|
||||
upper,
|
||||
inverted,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return voiCallbackArray;
|
||||
}
|
||||
|
||||
_setDisplaySets(
|
||||
viewportId: string,
|
||||
viewportData: StackData | VolumeData,
|
||||
viewportInfo: ViewportInfo
|
||||
): void {
|
||||
const viewport = this.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
|
||||
} else if (viewport instanceof VolumeViewport) {
|
||||
this._setVolumeViewport(
|
||||
viewport,
|
||||
viewportData as VolumeData,
|
||||
viewportInfo
|
||||
);
|
||||
} else {
|
||||
throw new Error('Unknown viewport type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the resize observer from the viewport element
|
||||
*/
|
||||
_removeResizeObserver() {
|
||||
if (this.viewportGridResizeObserver) {
|
||||
this.viewportGridResizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
_getSlabThickness(displaySetOptions, volumeId) {
|
||||
const { blendMode } = displaySetOptions;
|
||||
if (
|
||||
blendMode === undefined ||
|
||||
displaySetOptions.slabThickness === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is a slabThickness set as a number then use it
|
||||
if (typeof displaySetOptions.slabThickness === 'number') {
|
||||
return displaySetOptions.slabThickness;
|
||||
}
|
||||
|
||||
if (displaySetOptions.slabThickness.toLowerCase() === 'fullvolume') {
|
||||
// calculate the slab thickness based on the volume dimensions
|
||||
const imageVolume = cache.getVolume(volumeId);
|
||||
|
||||
const { dimensions } = imageVolume;
|
||||
const slabThickness = Math.sqrt(
|
||||
dimensions[0] * dimensions[0] +
|
||||
dimensions[1] * dimensions[1] +
|
||||
dimensions[2] * dimensions[2]
|
||||
);
|
||||
|
||||
return slabThickness;
|
||||
}
|
||||
}
|
||||
|
||||
_getViewportAndDisplaySetOptions(
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
publicDisplaySetOptions: DisplaySetOptions[],
|
||||
viewportInfo: ViewportInfo
|
||||
): {
|
||||
viewportOptions: ViewportOptions;
|
||||
displaySetOptions: DisplaySetOptions[];
|
||||
} {
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
|
||||
// Creating a temporary viewportInfo to handle defaults
|
||||
const newViewportInfo = new ViewportInfo(
|
||||
viewportIndex,
|
||||
viewportInfo.getViewportId()
|
||||
);
|
||||
|
||||
// To handle setting the default values if missing for the viewportOptions and
|
||||
// displaySetOptions
|
||||
newViewportInfo.setPublicViewportOptions(publicViewportOptions);
|
||||
newViewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
||||
|
||||
const newViewportOptions = newViewportInfo.getViewportOptions();
|
||||
const newDisplaySetOptions = newViewportInfo.getDisplaySetOptions();
|
||||
|
||||
return {
|
||||
viewportOptions: newViewportOptions,
|
||||
displaySetOptions: newDisplaySetOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function ExtendedCornerstoneViewportService(serviceManager) {
|
||||
return {
|
||||
name: 'CornerstoneViewportService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new CornerstoneViewportService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Types } from '@cornerstonejs/core';
|
||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||
import {
|
||||
DisplaySetOptions,
|
||||
PublicViewportOptions,
|
||||
ViewportOptions,
|
||||
} from './Viewport';
|
||||
|
||||
/**
|
||||
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||
* updating the viewport.
|
||||
*/
|
||||
export interface IViewportService {
|
||||
servicesManager: unknown;
|
||||
HangingProtocolService: unknown;
|
||||
renderingEngine: unknown;
|
||||
viewportGridResizeObserver: unknown;
|
||||
viewportsInfo: unknown;
|
||||
sceneVolumeInputs: unknown;
|
||||
viewportIndexUIDMap: unknown;
|
||||
viewportDivElements: unknown;
|
||||
ViewportPropertiesMap: unknown;
|
||||
volumeUIDs: unknown;
|
||||
listeners: { [key: string]: {} };
|
||||
displaySetsNeedRerendering: unknown;
|
||||
viewportDisplaySets: unknown;
|
||||
EVENTS: { [key: string]: string };
|
||||
_broadcastEvent: unknown;
|
||||
/**
|
||||
* Adds the HTML element to the viewportService
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} elementRef
|
||||
*/
|
||||
enableElement(
|
||||
viewportIndex: number,
|
||||
viewportOptions: ViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
): void;
|
||||
/**
|
||||
* It retrieves the renderingEngine if it does exist, or creates one otherwise
|
||||
* @returns {RenderingEngine} rendering engine
|
||||
*/
|
||||
getRenderingEngine(): Types.IRenderingEngine;
|
||||
/**
|
||||
* It creates a resize observer for the viewport element, and observes
|
||||
* the element for resizing events
|
||||
* @param {*} elementRef
|
||||
*/
|
||||
resize(element: HTMLDivElement): void;
|
||||
/**
|
||||
* Removes the viewport from cornerstone, and destroys the rendering engine
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Disables the viewport inside the renderingEngine, if no viewport is left
|
||||
* it destroys the renderingEngine.
|
||||
* @param viewportIndex
|
||||
*/
|
||||
disableElement(viewportIndex: number): void;
|
||||
/**
|
||||
* Uses the renderingEngine to enable the element for the given viewport index
|
||||
* and sets the displaySet data to the viewport
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} displaySet
|
||||
* @param {*} dataSource
|
||||
* @returns
|
||||
*/
|
||||
setViewportDisplaySets(
|
||||
viewportIndex: number,
|
||||
viewportData: StackData | VolumeData,
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
publicDisplaySetOptions: DisplaySetOptions[]
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { Types, Enums, CONSTANTS } from '@cornerstonejs/core';
|
||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||
|
||||
export type InitialImageOptions = {
|
||||
index?: number;
|
||||
preset?: JumpPresets;
|
||||
};
|
||||
|
||||
export type ViewportOptions = {
|
||||
viewportType: Enums.ViewportType;
|
||||
toolGroupId: string;
|
||||
viewportId: string;
|
||||
orientation?: Types.Orientation;
|
||||
background?: Types.Point3;
|
||||
initialView?: string;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
customViewportOptions?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PublicViewportOptions = {
|
||||
viewportType?: string;
|
||||
toolGroupId?: string;
|
||||
viewportId?: string;
|
||||
orientation?: string;
|
||||
background?: Types.Point3;
|
||||
initialView?: string;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: InitialImageOptions;
|
||||
customViewportOptions?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PublicDisplaySetOptions = {
|
||||
voi?: VOI;
|
||||
voiInverted?: boolean;
|
||||
blendMode?: string;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
};
|
||||
|
||||
export type DisplaySetOptions = {
|
||||
voi?: VOI;
|
||||
voiInverted: boolean;
|
||||
blendMode?: Enums.BlendModes;
|
||||
slabThickness?: number;
|
||||
colormap?: string;
|
||||
};
|
||||
|
||||
type VOI = {
|
||||
windowWidth: number;
|
||||
windowCenter: number;
|
||||
};
|
||||
|
||||
export type DisplaySet = {
|
||||
displaySetInstanceUID: string;
|
||||
};
|
||||
|
||||
const STACK = 'stack';
|
||||
const VOLUME = 'volume';
|
||||
const DEFAULT_TOOLGROUP_ID = 'default';
|
||||
|
||||
class ViewportInfo {
|
||||
private viewportId = '';
|
||||
private viewportIndex: number;
|
||||
private element: HTMLDivElement;
|
||||
private viewportOptions: ViewportOptions;
|
||||
private displaySetOptions: Array<DisplaySetOptions>;
|
||||
private renderingEngineId: string;
|
||||
|
||||
constructor(viewportIndex: number, viewportId: string) {
|
||||
this.viewportIndex = viewportIndex;
|
||||
this.viewportId = viewportId;
|
||||
this.setPublicViewportOptions({});
|
||||
this.setPublicDisplaySetOptions([{}]);
|
||||
}
|
||||
|
||||
public setRenderingEngineId(renderingEngineId: string): void {
|
||||
this.renderingEngineId = renderingEngineId;
|
||||
}
|
||||
|
||||
public getRenderingEngineId(): string {
|
||||
return this.renderingEngineId;
|
||||
}
|
||||
|
||||
public setViewportId(viewportId: string): void {
|
||||
this.viewportId = viewportId;
|
||||
}
|
||||
public setViewportIndex(viewportIndex: number): void {
|
||||
this.viewportIndex = viewportIndex;
|
||||
}
|
||||
|
||||
public setElement(element: HTMLDivElement): void {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
public getViewportIndex(): number {
|
||||
return this.viewportIndex;
|
||||
}
|
||||
|
||||
public getElement(): HTMLDivElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
public getViewportId(): string {
|
||||
return this.viewportId;
|
||||
}
|
||||
|
||||
public setPublicDisplaySetOptions(
|
||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||
): void {
|
||||
// map the displaySetOptions and check if they are undefined then set them to default values
|
||||
const displaySetOptions = this.mapDisplaySetOptions(
|
||||
publicDisplaySetOptions
|
||||
);
|
||||
|
||||
this.setDisplaySetOptions(displaySetOptions);
|
||||
}
|
||||
|
||||
public setPublicViewportOptions(
|
||||
viewportOptionsEntry: PublicViewportOptions
|
||||
): void {
|
||||
let viewportType = viewportOptionsEntry.viewportType;
|
||||
let toolGroupId = viewportOptionsEntry.toolGroupId;
|
||||
let orientation;
|
||||
|
||||
if (!viewportType) {
|
||||
viewportType = getCornerstoneViewportType(STACK);
|
||||
} else {
|
||||
viewportType = getCornerstoneViewportType(
|
||||
viewportOptionsEntry.viewportType
|
||||
);
|
||||
}
|
||||
|
||||
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
||||
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||
} else {
|
||||
orientation = CONSTANTS.ORIENTATION.AXIAL;
|
||||
}
|
||||
|
||||
if (!toolGroupId) {
|
||||
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||
}
|
||||
|
||||
this.setViewportOptions({
|
||||
...viewportOptionsEntry,
|
||||
viewportId: this.viewportId,
|
||||
viewportType: viewportType as Enums.ViewportType,
|
||||
orientation,
|
||||
toolGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
public setViewportOptions(viewportOptions: ViewportOptions): void {
|
||||
this.viewportOptions = viewportOptions;
|
||||
}
|
||||
|
||||
public getViewportOptions(): ViewportOptions {
|
||||
return this.viewportOptions;
|
||||
}
|
||||
|
||||
public setDisplaySetOptions(
|
||||
displaySetOptions: Array<DisplaySetOptions>
|
||||
): void {
|
||||
this.displaySetOptions = displaySetOptions;
|
||||
}
|
||||
|
||||
public getSyncGroups(): SyncGroup[] {
|
||||
return this.viewportOptions.syncGroups || [];
|
||||
}
|
||||
|
||||
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
||||
return this.displaySetOptions;
|
||||
}
|
||||
|
||||
public getViewportType(): Enums.ViewportType {
|
||||
return this.viewportOptions.viewportType || Enums.ViewportType.STACK;
|
||||
}
|
||||
|
||||
public getToolGroupId(): string {
|
||||
return this.viewportOptions.toolGroupId;
|
||||
}
|
||||
|
||||
public getBackground(): Types.Point3 {
|
||||
return this.viewportOptions.background || [0, 0, 0];
|
||||
}
|
||||
|
||||
public getOrientation(): Types.Orientation {
|
||||
return this.viewportOptions.orientation;
|
||||
}
|
||||
|
||||
public getInitialImageOptions(): InitialImageOptions {
|
||||
return this.viewportOptions.initialImageOptions;
|
||||
}
|
||||
|
||||
private mapDisplaySetOptions(
|
||||
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||
): Array<DisplaySetOptions> {
|
||||
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||
|
||||
publicDisplaySetOptions.forEach(option => {
|
||||
const blendMode = getCornerstoneBlendMode(option.blendMode);
|
||||
|
||||
displaySetOptions.push({
|
||||
voi: option.voi || ({} as VOI),
|
||||
voiInverted: option.voiInverted || false,
|
||||
colormap: option.colormap || undefined,
|
||||
slabThickness: option.slabThickness,
|
||||
blendMode,
|
||||
});
|
||||
});
|
||||
|
||||
return displaySetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewportInfo;
|
||||
@@ -0,0 +1,3 @@
|
||||
const RENDERING_ENGINE_ID = 'OHIFCornerstoneRenderingEngine';
|
||||
|
||||
export { RENDERING_ENGINE_ID };
|
||||
Reference in new issue
Block a user