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:
authored and GitHub committed 2022-07-27 12:39:04 -04:00
1 parent 86dcaf6293
commit bd1c38c00c
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 };