Pull in master
This commit is contained in:
commit
e7501933dd
@ -1,112 +1,138 @@
|
|||||||
|
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||||
|
import { IWebApiDataSource } from '@ohif/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
/**
|
id: 'org.ohif.*',
|
||||||
* Only required property. Should be a unique value across all extensions.
|
|
||||||
*/
|
|
||||||
id: 'example-extension',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LIFECYCLE HOOKS
|
* LIFECYCLE HOOKS
|
||||||
*/
|
*/
|
||||||
|
preRegistration() {},
|
||||||
preRegistration({
|
beforeExtInit() {},
|
||||||
servicesManager = {},
|
beforeExtDestroy() {},
|
||||||
commandsManager = {},
|
|
||||||
appConfig = {},
|
|
||||||
configuration = {},
|
|
||||||
}) {},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MODULE GETTERS
|
* MODULES
|
||||||
*/
|
*/
|
||||||
getViewportModule() {
|
getCommandsModule,
|
||||||
return '... react component ...';
|
getContextModule,
|
||||||
},
|
getDataSourcesModule,
|
||||||
getSopClassHandlerModule() {
|
getLayoutTemplateModule,
|
||||||
return sopClassHandlerModule;
|
getPanelModule,
|
||||||
},
|
getSopClassHandlerModule,
|
||||||
getPanelModule() {
|
getToolbarModule() {},
|
||||||
return panelModule;
|
getViewportModule,
|
||||||
},
|
|
||||||
getToolbarModule() {
|
|
||||||
return panelModule;
|
|
||||||
},
|
|
||||||
getCommandsModule(/* store */) {
|
|
||||||
return commandsModule;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// appConfig,
|
||||||
|
// extensionConfig,
|
||||||
|
// dataSources,
|
||||||
|
// servicesManager,
|
||||||
|
// extensionManager,
|
||||||
|
// commandsManager,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const commandsModule = {
|
const getCommandsModule = () => ({
|
||||||
actions: {
|
|
||||||
// Store Contexts + Options
|
|
||||||
exampleAction: ({ viewports, param1 }) => {
|
|
||||||
console.log(`There are ${viewports.length} viewports`);
|
|
||||||
console.log(`param1's value is: ${param1}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
definitions: {
|
definitions: {
|
||||||
exampleActionDef: {
|
exampleActionDef: {
|
||||||
commandFn: this.actions.exampleAction,
|
commandFn: ({ param1 }) => {
|
||||||
storeContexts: ['viewports'],
|
console.log(`param1's value is: ${param1}`);
|
||||||
|
},
|
||||||
|
// storeContexts: ['viewports'],
|
||||||
options: { param1: 'hello world' },
|
options: { param1: 'hello world' },
|
||||||
|
context: 'VIEWER', // optional
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
const ExampleContext = React.createContext();
|
||||||
*
|
|
||||||
*/
|
|
||||||
const sopClassHandlerModule = {
|
|
||||||
id: 'OHIFDicomHtmlSopClassHandler',
|
|
||||||
sopClassUIDs: Object.values({
|
|
||||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
|
||||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
|
||||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
|
||||||
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
|
||||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
|
||||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
|
||||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
|
||||||
}),
|
|
||||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
|
||||||
const instance = series.getFirstInstance();
|
|
||||||
|
|
||||||
return {
|
function ExampleContextProvider({ children }) {
|
||||||
plugin: 'html',
|
return (
|
||||||
displaySetInstanceUID: 0, //utils.guid(),
|
<ExampleContext.Provider value={{ example: 'value' }}>
|
||||||
wadoRoot: study.getData().wadoRoot,
|
{children}
|
||||||
wadoUri: instance.getData().wadouri,
|
</TrackedMeasurementsContext.Provider>
|
||||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
);
|
||||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
}
|
||||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
|
||||||
authorizationHeaders,
|
const getContextModule = () => [
|
||||||
};
|
{
|
||||||
|
name: 'ExampleContext',
|
||||||
|
context: ExampleContext,
|
||||||
|
provider: ExampleContextProvider,
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getDataSourcesModule = () => [
|
||||||
|
{
|
||||||
|
name: 'exampleDataSource',
|
||||||
|
type: 'webApi', // 'webApi' | 'local' | 'other'
|
||||||
|
createDataSource: dataSourceConfig => {
|
||||||
|
return IWebApiDataSource.create(/* */);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getLayoutTemplateModule = (/* ... */) => [
|
||||||
|
{
|
||||||
|
id: 'exampleLayout',
|
||||||
|
name: 'exampleLayout',
|
||||||
|
component: ExampleLayoutComponent,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getPanelModule = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'exampleSidePanel',
|
||||||
|
iconName: 'info-circle-o',
|
||||||
|
iconLabel: 'Example',
|
||||||
|
label: 'Hello World',
|
||||||
|
isDisabled: studies => {}, // optional
|
||||||
|
component: ExamplePanelContentComponent,
|
||||||
|
},
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const getSopClassHandlerModule = (/* ... */) => {
|
||||||
*
|
const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11';
|
||||||
*/
|
|
||||||
const panelModule = {
|
return [
|
||||||
menuOptions: [
|
|
||||||
{
|
{
|
||||||
icon: 'th-list',
|
name: 'ExampleSopClassHandle',
|
||||||
label: 'Segments',
|
sopClassUids: [BASIC_TEXT_SR],
|
||||||
target: 'segment-panel',
|
getDisplaySetsFromSeries: instances => {
|
||||||
isDisabled: studies => {
|
const imageSet = new ImageSet(instances);
|
||||||
return false;
|
|
||||||
|
imageSet.setAttributes(/** */);
|
||||||
|
imageSet.sortBy((a, b) => 0);
|
||||||
|
|
||||||
|
return imageSet;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
components: [
|
};
|
||||||
{
|
|
||||||
id: 'segment-panel',
|
const getToolbarModule = () => {};
|
||||||
component: '... react component ...',
|
|
||||||
},
|
// displaySet, viewportIndex, dataSource
|
||||||
],
|
const getViewportModule = () => {
|
||||||
defaultContext: ['VIEWER'],
|
const wrappedViewport = props => {
|
||||||
|
return (
|
||||||
|
<ExampleViewport
|
||||||
|
{...props}
|
||||||
|
onEvent={data => {
|
||||||
|
commandsManager.runCommand('commandName', data);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{ name: 'example', component: wrappedViewport }];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -145,4 +145,8 @@ CornerstoneViewportDownloadForm.propTypes = {
|
|||||||
activeViewportIndex: PropTypes.number.isRequired,
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CornerstoneViewportDownloadForm;
|
// export default CornerstoneViewportDownloadForm;
|
||||||
|
|
||||||
|
export default function HelloWorld() {
|
||||||
|
return <div>Hello World</div>;
|
||||||
|
}
|
||||||
|
|||||||
@ -148,13 +148,13 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||||
return enabledElement;
|
return enabledElement;
|
||||||
},
|
},
|
||||||
showDownloadViewportModal: ({ title, viewports }) => {
|
showDownloadViewportModal: () => {
|
||||||
const activeViewportIndex = viewports.activeViewportIndex;
|
const activeViewportIndex = 1; // viewports.activeViewportIndex;
|
||||||
const { UIModalService } = servicesManager.services;
|
const { UIModalService } = servicesManager.services;
|
||||||
if (UIModalService) {
|
if (UIModalService) {
|
||||||
UIModalService.show({
|
UIModalService.show({
|
||||||
content: CornerstoneViewportDownloadForm,
|
content: CornerstoneViewportDownloadForm,
|
||||||
title,
|
title: 'Download High Quality Image',
|
||||||
contentProps: {
|
contentProps: {
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
onClose: UIModalService.hide,
|
onClose: UIModalService.hide,
|
||||||
@ -318,7 +318,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
},
|
},
|
||||||
showDownloadViewportModal: {
|
showDownloadViewportModal: {
|
||||||
commandFn: actions.showDownloadViewportModal,
|
commandFn: actions.showDownloadViewportModal,
|
||||||
storeContexts: ['viewports'],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
getActiveViewportEnabledElement: {
|
getActiveViewportEnabledElement: {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import init from './init.js';
|
import init from './init.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
|
||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
@ -48,9 +47,6 @@ export default {
|
|||||||
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
|
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
getToolbarModule() {
|
|
||||||
return toolbarModule;
|
|
||||||
},
|
|
||||||
getCommandsModule({ servicesManager }) {
|
getCommandsModule({ servicesManager }) {
|
||||||
return commandsModule({ servicesManager });
|
return commandsModule({ servicesManager });
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,14 +1,3 @@
|
|||||||
// TODO: A way to add Icons that don't already exist?
|
|
||||||
// - Register them and add
|
|
||||||
// - Include SVG Source/Inline?
|
|
||||||
// - By URL, or own component?
|
|
||||||
|
|
||||||
// What KINDS of toolbar buttons do we have...
|
|
||||||
// - One's that dispatch commands
|
|
||||||
// - One's that set tool's active
|
|
||||||
// - More custom, like CINE
|
|
||||||
// - Built in for one's like this, or custom components?
|
|
||||||
|
|
||||||
// Visible?
|
// Visible?
|
||||||
// Disabled?
|
// Disabled?
|
||||||
// Based on contexts or misc. criteria?
|
// Based on contexts or misc. criteria?
|
||||||
@ -17,241 +6,118 @@
|
|||||||
// setToolActive commands should receive the button event that triggered
|
// setToolActive commands should receive the button event that triggered
|
||||||
// so we can do the "bind to this button" magic
|
// so we can do the "bind to this button" magic
|
||||||
|
|
||||||
const TOOLBAR_BUTTON_TYPES = {
|
// const definitions = [
|
||||||
COMMAND: 'command',
|
// // OLD
|
||||||
SET_TOOL_ACTIVE: 'setToolActive',
|
// {
|
||||||
BUILT_IN: 'builtIn',
|
// id: 'StackScroll',
|
||||||
};
|
// label: 'Stack Scroll',
|
||||||
|
// icon: 'bars',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
// commandName: 'setToolActive',
|
||||||
|
// commandOptions: { toolName: 'StackScroll' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Reset',
|
||||||
|
// label: 'Reset',
|
||||||
|
// icon: 'reset',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'resetViewport',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Cine',
|
||||||
|
// label: 'CINE',
|
||||||
|
// icon: 'youtube',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||||
|
// options: {
|
||||||
|
// behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'More',
|
||||||
|
// label: 'More',
|
||||||
|
// icon: 'ellipse-circle',
|
||||||
|
// buttons: [
|
||||||
|
// {
|
||||||
|
// id: 'Magnify',
|
||||||
|
// label: 'Magnify',
|
||||||
|
// icon: 'circle',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
// commandName: 'setToolActive',
|
||||||
|
// commandOptions: { toolName: 'Magnify' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'WwwcRegion',
|
||||||
|
// label: 'ROI Window',
|
||||||
|
// icon: 'stop',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
// commandName: 'setToolActive',
|
||||||
|
// commandOptions: { toolName: 'WwwcRegion' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Invert',
|
||||||
|
// label: 'Invert',
|
||||||
|
// icon: 'adjust',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'invertViewport',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'RotateRight',
|
||||||
|
// label: 'Rotate Right',
|
||||||
|
// icon: 'rotate-right',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'rotateViewportCW',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'FlipH',
|
||||||
|
// label: 'Flip H',
|
||||||
|
// icon: 'ellipse-h',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'flipViewportHorizontal',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'FlipV',
|
||||||
|
// label: 'Flip V',
|
||||||
|
// icon: 'ellipse-v',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'flipViewportVertical',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Clear',
|
||||||
|
// label: 'Clear',
|
||||||
|
// icon: 'trash',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'clearAnnotations',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Eraser',
|
||||||
|
// label: 'Eraser',
|
||||||
|
// icon: 'eraser',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
// commandName: 'setToolActive',
|
||||||
|
// commandOptions: { toolName: 'Eraser' },
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'Exit2DMPR',
|
||||||
|
// label: 'Exit 2D MPR',
|
||||||
|
// icon: 'times',
|
||||||
|
// //
|
||||||
|
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
// commandName: 'setCornerstoneLayout',
|
||||||
|
// context: 'ACTIVE_VIEWPORT::VTK',
|
||||||
|
// },
|
||||||
|
// ];
|
||||||
|
|
||||||
const TOOLBAR_BUTTON_BEHAVIORS = {
|
export default [];
|
||||||
CINE: 'CINE',
|
|
||||||
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* TODO: Export enums through a extension manager. */
|
|
||||||
const enums = {
|
|
||||||
TOOLBAR_BUTTON_TYPES,
|
|
||||||
TOOLBAR_BUTTON_BEHAVIORS,
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions = [
|
|
||||||
// UPDATED
|
|
||||||
{
|
|
||||||
id: 'Zoom',
|
|
||||||
label: 'Zoom',
|
|
||||||
icon: 'tool-zoom',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Wwwc',
|
|
||||||
label: 'Levels',
|
|
||||||
icon: 'tool-window-level',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Wwwc' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Pan',
|
|
||||||
label: 'Pan',
|
|
||||||
icon: 'tool-move',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Pan' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Capture',
|
|
||||||
label: 'Capture',
|
|
||||||
icon: 'tool-capture',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
|
||||||
options: {
|
|
||||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
|
|
||||||
togglable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Annotate',
|
|
||||||
label: 'Annotate',
|
|
||||||
icon: 'tool-annotate',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Bidirectional',
|
|
||||||
label: 'Bidirectional',
|
|
||||||
icon: 'tool-bidirectional',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Bidirectional' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Ellipse',
|
|
||||||
label: 'Ellipse',
|
|
||||||
icon: 'tool-elipse',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'EllipticalRoi' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Length',
|
|
||||||
label: 'Length',
|
|
||||||
icon: 'tool-length',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Length' },
|
|
||||||
},
|
|
||||||
// OLD
|
|
||||||
{
|
|
||||||
id: 'StackScroll',
|
|
||||||
label: 'Stack Scroll',
|
|
||||||
icon: 'bars',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'StackScroll' },
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 'Angle',
|
|
||||||
label: 'Angle',
|
|
||||||
icon: 'angle-left',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Angle' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Reset',
|
|
||||||
label: 'Reset',
|
|
||||||
icon: 'reset',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'resetViewport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Cine',
|
|
||||||
label: 'CINE',
|
|
||||||
icon: 'youtube',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
|
||||||
options: {
|
|
||||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'More',
|
|
||||||
label: 'More',
|
|
||||||
icon: 'ellipse-circle',
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
id: 'Magnify',
|
|
||||||
label: 'Magnify',
|
|
||||||
icon: 'circle',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Magnify' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'WwwcRegion',
|
|
||||||
label: 'ROI Window',
|
|
||||||
icon: 'stop',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'WwwcRegion' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'DragProbe',
|
|
||||||
label: 'Probe',
|
|
||||||
icon: 'dot-circle',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'DragProbe' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'RectangleRoi',
|
|
||||||
label: 'Rectangle',
|
|
||||||
icon: 'square-o',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'RectangleRoi' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Invert',
|
|
||||||
label: 'Invert',
|
|
||||||
icon: 'adjust',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'invertViewport',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'RotateRight',
|
|
||||||
label: 'Rotate Right',
|
|
||||||
icon: 'rotate-right',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'rotateViewportCW',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'FlipH',
|
|
||||||
label: 'Flip H',
|
|
||||||
icon: 'ellipse-h',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'flipViewportHorizontal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'FlipV',
|
|
||||||
label: 'Flip V',
|
|
||||||
icon: 'ellipse-v',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'flipViewportVertical',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Clear',
|
|
||||||
label: 'Clear',
|
|
||||||
icon: 'trash',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'clearAnnotations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Eraser',
|
|
||||||
label: 'Eraser',
|
|
||||||
icon: 'eraser',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Eraser' },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Exit2DMPR',
|
|
||||||
label: 'Exit 2D MPR',
|
|
||||||
icon: 'times',
|
|
||||||
//
|
|
||||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
commandName: 'setCornerstoneLayout',
|
|
||||||
context: 'ACTIVE_VIEWPORT::VTK',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default {
|
|
||||||
definitions,
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
|
||||||
};
|
|
||||||
|
|||||||
7
extensions/default/src/Toolbar/ToolbarDivider.jsx
Normal file
7
extensions/default/src/Toolbar/ToolbarDivider.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function ToolbarDivider() {
|
||||||
|
return (
|
||||||
|
<span className="self-center w-1 h-8 mx-2 border-l border-common-dark" />
|
||||||
|
);
|
||||||
|
}
|
||||||
41
extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx
Normal file
41
extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||||
|
|
||||||
|
function LayoutSelector() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function LayoutSelector() {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('click', LayoutSelector);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', LayoutSelector);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const dropdownContent = isOpen ? OHIFLayoutSelector : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
id="Layout"
|
||||||
|
label="Grid Layout"
|
||||||
|
icon="tool-layout"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
dropdownContent={dropdownContent}
|
||||||
|
isActive={isOpen}
|
||||||
|
type="primary"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutSelector.propTypes = {
|
||||||
|
children: PropTypes.any.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutSelector;
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
//
|
//
|
||||||
import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui';
|
import { NavBar, Svg, Icon, IconButton } from '@ohif/ui';
|
||||||
|
|
||||||
function Header({ tools, moreTools }) {
|
function Header({ children }) {
|
||||||
const [activeTool, setActiveTool] = useState('Zoom');
|
|
||||||
// const dropdownContent = [
|
// const dropdownContent = [
|
||||||
// {
|
// {
|
||||||
// name: 'Soft tissue',
|
// name: 'Soft tissue',
|
||||||
@ -14,80 +13,16 @@ function Header({ tools, moreTools }) {
|
|||||||
// { name: 'Liver', value: '150 / 90' },
|
// { name: 'Liver', value: '150 / 90' },
|
||||||
// { name: 'Bone', value: '2500 / 480' },
|
// { name: 'Bone', value: '2500 / 480' },
|
||||||
// { name: 'Brain', value: '80 / 40' },
|
// { name: 'Brain', value: '80 / 40' },
|
||||||
// ];
|
// ]
|
||||||
|
|
||||||
// TODO -> In ToolBarManager => Consume commandName and commandOptions and create onClick?
|
|
||||||
|
|
||||||
/*
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
id: 'Zoom',
|
|
||||||
label: 'Zoom',
|
|
||||||
icon: 'tool-zoom',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
onClick: () => setActiveTool('Zoom'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Wwwc',
|
|
||||||
label: 'Levels',
|
|
||||||
icon: 'tool-window-level',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Wwwc' },
|
|
||||||
onClick: () => setActiveTool('Wwwc'),
|
|
||||||
dropdownContent: (
|
|
||||||
<div>
|
|
||||||
{dropdownContent.map((row, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="text-base text-white">{row.name}</span>
|
|
||||||
<span className="text-base text-primary-light ml-3">
|
|
||||||
{row.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Pan',
|
|
||||||
label: 'Pan',
|
|
||||||
icon: 'tool-move',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Pan' },
|
|
||||||
onClick: () => setActiveTool('Pan'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Capture',
|
|
||||||
label: 'Capture',
|
|
||||||
icon: 'tool-capture',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Capture' },
|
|
||||||
onClick: () => setActiveTool('Capture'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Layout',
|
|
||||||
label: 'Layout',
|
|
||||||
icon: 'tool-layout',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Layout' },
|
|
||||||
onClick: () => setActiveTool('Layout'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
*/
|
|
||||||
return (
|
return (
|
||||||
<NavBar className="justify-between border-b-4 border-black">
|
<NavBar className="justify-between border-b-4 border-black">
|
||||||
<div className="flex flex-1 justify-between">
|
<div className="flex justify-between flex-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="mr-3 inline-flex items-center">
|
<div className="inline-flex items-center mr-3">
|
||||||
<Icon
|
<Icon
|
||||||
name="chevron-left"
|
name="chevron-left"
|
||||||
className="text-primary-active w-8 cursor-pointer"
|
className="w-8 cursor-pointer text-primary-active"
|
||||||
onClick={() => alert('Navigate to previous page')}
|
onClick={() => alert('Navigate to previous page')}
|
||||||
/>
|
/>
|
||||||
<a href="#" className="ml-4">
|
<a href="#" className="ml-4">
|
||||||
@ -95,15 +30,9 @@ function Header({ tools, moreTools }) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center">{children}</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Toolbar
|
<span className="mr-3 text-lg text-common-light">
|
||||||
tools={tools}
|
|
||||||
activeTool={activeTool}
|
|
||||||
moreTools={moreTools}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="mr-3 text-common-light text-lg">
|
|
||||||
FOR INVESTIGATIONAL USE ONLY
|
FOR INVESTIGATIONAL USE ONLY
|
||||||
</span>
|
</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -123,8 +52,7 @@ function Header({ tools, moreTools }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Header.propTypes = {
|
Header.propTypes = {
|
||||||
tools: PropTypes.array.isRequired,
|
children: PropTypes.any.isRequired,
|
||||||
moreTools: PropTypes.array.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
|
|
||||||
|
function NestedMenu({ children }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function closeNestedMenu() {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('click', closeNestedMenu);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', closeNestedMenu);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const dropdownContent = isOpen ? children : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
id="More"
|
||||||
|
label="More"
|
||||||
|
icon="tool-more-menu"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}}
|
||||||
|
dropdownContent={dropdownContent}
|
||||||
|
isActive={isOpen}
|
||||||
|
type="primary"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NestedMenu.propTypes = {
|
||||||
|
children: PropTypes.any.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NestedMenu;
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SidePanel, Toolbar } from '@ohif/ui';
|
import { SidePanel } from '@ohif/ui';
|
||||||
//
|
|
||||||
import Header from './Header.jsx';
|
import Header from './Header.jsx';
|
||||||
|
import NestedMenu from './ToolbarButtonNestedMenu.jsx';
|
||||||
|
|
||||||
function ViewerLayout({
|
function ViewerLayout({
|
||||||
// From Extension Module Params
|
// From Extension Module Params
|
||||||
@ -54,71 +55,23 @@ function ViewerLayout({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToolBarSubscription = newToolBarLayout => {
|
const [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
|
||||||
// Get buttons to pass to toolbars.
|
|
||||||
console.log(commandsManager);
|
|
||||||
|
|
||||||
const firstTool = newToolBarLayout[0].tools[0];
|
|
||||||
|
|
||||||
const toolBarLayout = [];
|
|
||||||
|
|
||||||
newToolBarLayout.forEach(newToolBar => {
|
|
||||||
const toolBar = { tools: [], moreTools: [] };
|
|
||||||
|
|
||||||
Object.keys(newToolBar).forEach(key => {
|
|
||||||
if (newToolBar[key].length) {
|
|
||||||
newToolBar[key].forEach(tool => {
|
|
||||||
const commandOptions = tool.commandOptions || {};
|
|
||||||
|
|
||||||
toolBar[key].push({
|
|
||||||
context: tool.context,
|
|
||||||
icon: tool.icon,
|
|
||||||
id: tool.id,
|
|
||||||
label: tool.label,
|
|
||||||
type: 'setToolActive',
|
|
||||||
onClick: () => {
|
|
||||||
commandsManager.runCommand(tool.commandName, commandOptions);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
toolBarLayout.push(toolBar);
|
|
||||||
|
|
||||||
// if (newToolBar.moreTools && newToolBar.moreTools.length) {
|
|
||||||
// newToolBar.moreTools.forEach(tool => {
|
|
||||||
// const commandOptions = tool.commandOptions || {};
|
|
||||||
|
|
||||||
// toolBar.push({
|
|
||||||
// context: tool.context,
|
|
||||||
// icon: tool.icon,
|
|
||||||
// id: tool.id,
|
|
||||||
// label: tool.label,
|
|
||||||
// type: 'setToolActive',
|
|
||||||
// command: () =>
|
|
||||||
// commandsManager.runCommand(tool.commandName, commandOptions),
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
setToolBarLayout(toolBarLayout);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [toolBarLayout, setToolBarLayout] = useState([
|
|
||||||
{ tools: [], moreTools: [] },
|
|
||||||
{ tools: [] },
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = ToolBarService.subscribe(
|
const { unsubscribe } = ToolBarService.subscribe(
|
||||||
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
|
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||||
handleToolBarSubscription
|
() => {
|
||||||
|
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
|
||||||
|
const updatedToolbars = {
|
||||||
|
primary: ToolBarService.getButtonSection('primary'),
|
||||||
|
secondary: ToolBarService.getButtonSection('secondary'),
|
||||||
|
};
|
||||||
|
setToolbars(updatedToolbars);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, []);
|
}, [ToolBarService]);
|
||||||
|
|
||||||
const leftPanelComponents = leftPanels.map(getPanelData);
|
const leftPanelComponents = leftPanels.map(getPanelData);
|
||||||
const rightPanelComponents = rightPanels.map(getPanelData);
|
const rightPanelComponents = rightPanels.map(getPanelData);
|
||||||
@ -126,10 +79,30 @@ function ViewerLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header
|
<Header>
|
||||||
tools={toolBarLayout[0].tools}
|
<div className="relative flex justify-center">
|
||||||
moreTools={toolBarLayout[0].moreTools}
|
{toolbars.primary.map(toolDef => {
|
||||||
/>
|
const isNested = Array.isArray(toolDef);
|
||||||
|
|
||||||
|
if (!isNested) {
|
||||||
|
const { id, Component, componentProps } = toolDef;
|
||||||
|
|
||||||
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<NestedMenu>
|
||||||
|
<div className="flex">
|
||||||
|
{toolDef.map(x => {
|
||||||
|
const { id, Component, componentProps } = x;
|
||||||
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</NestedMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row flex-no-wrap items-stretch w-full overflow-hidden"
|
className="flex flex-row flex-no-wrap items-stretch w-full overflow-hidden"
|
||||||
style={{ height: 'calc(100vh - 57px' }}
|
style={{ height: 'calc(100vh - 57px' }}
|
||||||
@ -145,67 +118,17 @@ function ViewerLayout({
|
|||||||
{/* TOOLBAR + GRID */}
|
{/* TOOLBAR + GRID */}
|
||||||
<div className="flex flex-col flex-1 h-full">
|
<div className="flex flex-col flex-1 h-full">
|
||||||
<div className="flex h-12 border-b border-transparent flex-2 w-100">
|
<div className="flex h-12 border-b border-transparent flex-2 w-100">
|
||||||
<Toolbar type="secondary" tools={toolBarLayout[1].tools} />
|
{toolbars.secondary.map(toolDef => {
|
||||||
|
const { id, Component, componentProps } = toolDef;
|
||||||
|
|
||||||
|
return <Component key={id} id={id} {...componentProps} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center flex-1 h-full pt-1 pb-2 overflow-hidden bg-black">
|
<div className="flex items-center justify-center flex-1 h-full pt-1 pb-2 overflow-hidden bg-black">
|
||||||
<ViewportGridComp
|
<ViewportGridComp
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
viewportComponents={viewportComponents}
|
viewportComponents={viewportComponents}
|
||||||
/>
|
/>
|
||||||
{/*
|
|
||||||
viewportContents={[
|
|
||||||
<Viewport
|
|
||||||
viewportIndex={0}
|
|
||||||
onSeriesChange={direction => alert(`Series ${direction}`)}
|
|
||||||
studyData={{
|
|
||||||
label: 'A',
|
|
||||||
isTracked: true,
|
|
||||||
isLocked: false,
|
|
||||||
studyDate: '07-Sep-2011',
|
|
||||||
currentSeries: 1,
|
|
||||||
seriesDescription:
|
|
||||||
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
|
|
||||||
modality: 'CT',
|
|
||||||
patientInformation: {
|
|
||||||
patientName: 'Smith, Jane',
|
|
||||||
patientSex: 'F',
|
|
||||||
patientAge: '59',
|
|
||||||
MRN: '10000001',
|
|
||||||
thickness: '5.0mm',
|
|
||||||
spacing: '1.25mm',
|
|
||||||
scanner: 'Aquilion',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Viewport>,
|
|
||||||
<Viewport
|
|
||||||
viewportIndex={1}
|
|
||||||
onSeriesChange={direction => alert(`Series ${direction}`)}
|
|
||||||
studyData={{
|
|
||||||
label: 'A',
|
|
||||||
isTracked: false,
|
|
||||||
isLocked: true,
|
|
||||||
studyDate: '07-Sep-2010',
|
|
||||||
currentSeries: 2,
|
|
||||||
seriesDescription:
|
|
||||||
'Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit Series description lorem ipsum dolor sit ',
|
|
||||||
modality: 'SR',
|
|
||||||
patientInformation: {
|
|
||||||
patientName: 'Smith, Jane',
|
|
||||||
patientSex: 'F',
|
|
||||||
patientAge: '59',
|
|
||||||
MRN: '10000001',
|
|
||||||
thickness: '2.0mm',
|
|
||||||
spacing: '1.25mm',
|
|
||||||
scanner: 'Aquilion',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Viewport>,
|
|
||||||
]}
|
|
||||||
setActiveViewportIndex={setActiveViewportIndex}
|
|
||||||
activeViewportIndex={activeViewportIndex}
|
|
||||||
/>*/}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{rightPanelComponents.length && (
|
{rightPanelComponents.length && (
|
||||||
@ -227,14 +150,6 @@ ViewerLayout.propTypes = {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
commandsManager: PropTypes.object,
|
commandsManager: PropTypes.object,
|
||||||
// From modes
|
// From modes
|
||||||
// TODO: Not in love with this shape,
|
|
||||||
toolBarLayout: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
tools: PropTypes.array,
|
|
||||||
moreTools: PropTypes.array,
|
|
||||||
})
|
|
||||||
).isRequired,
|
|
||||||
//displaySetInstanceUids: PropTypes.any.isRequired,
|
|
||||||
leftPanels: PropTypes.array,
|
leftPanels: PropTypes.array,
|
||||||
rightPanels: PropTypes.array,
|
rightPanels: PropTypes.array,
|
||||||
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
// SEE:
|
|
||||||
// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
|
||||||
import React from 'react';
|
|
||||||
import { useViewportGrid } from '@ohif/ui';
|
|
||||||
|
|
||||||
function getCommandsModule({ servicesManager }) {
|
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
const definitions = {
|
|
||||||
toggleLayoutSelectionDialog: {
|
|
||||||
commandFn: () => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
window.alert(
|
|
||||||
'Unable to show dialog; no UI Dialog Service available.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use SimpleDialog component
|
|
||||||
// TODO: update position on window resize
|
|
||||||
// TODO: Expand service API to check if dialog w/ ID is already open
|
|
||||||
// TODO: Import and call `useViewportGrid`
|
|
||||||
UIDialogService.dismiss({ id: 'layoutSelection' });
|
|
||||||
UIDialogService.create({
|
|
||||||
id: 'layoutSelection',
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: Test,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
context: 'VIEWER',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
definitions,
|
|
||||||
defaultContext: 'VIEWER',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test() {
|
|
||||||
const [
|
|
||||||
{ numCols, numRows, activeViewportIndex, viewports },
|
|
||||||
dispatch,
|
|
||||||
] = useViewportGrid();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_LAYOUT',
|
|
||||||
payload: {
|
|
||||||
numCols: 2,
|
|
||||||
numRows: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{ color: 'white' }}
|
|
||||||
>
|
|
||||||
Hello World!
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCommandsModule;
|
|
||||||
@ -1,19 +1,98 @@
|
|||||||
const TOOLBAR_BUTTON_TYPES = {
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
COMMAND: 'command',
|
import ToolbarDivider from './Toolbar/ToolbarDivider.jsx';
|
||||||
SET_TOOL_ACTIVE: 'setToolActive',
|
import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.jsx';
|
||||||
BUILT_IN: 'builtIn',
|
|
||||||
};
|
|
||||||
|
|
||||||
const definitions = [
|
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||||
{
|
const toolbarService = servicesManager.services.ToolBarService;
|
||||||
id: 'Layout',
|
|
||||||
label: 'Layout',
|
|
||||||
icon: 'tool-layout',
|
|
||||||
commandName: 'toggleLayoutSelectionDialog',
|
|
||||||
commandOptions: { },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function getToolbarModule() {
|
return [
|
||||||
return { definitions, defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE' };
|
{
|
||||||
|
name: 'ohif.divider',
|
||||||
|
defaultComponent: ToolbarDivider,
|
||||||
|
clickHandler: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ohif.action',
|
||||||
|
defaultComponent: ToolbarButton,
|
||||||
|
requiredConfig: [],
|
||||||
|
optionalConfig: [],
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: [],
|
||||||
|
clickHandler: (evt, btn, btnSectionName) => {
|
||||||
|
const { props } = btn;
|
||||||
|
commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ohif.radioGroup',
|
||||||
|
defaultComponent: ToolbarButton,
|
||||||
|
requiredConfig: ['groupName'],
|
||||||
|
optionalConfig: [],
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: [],
|
||||||
|
clickHandler: (evt, clickedBtn, btnSectionName) => {
|
||||||
|
const { props } = clickedBtn;
|
||||||
|
const allButtons = toolbarService.getButtons();
|
||||||
|
|
||||||
|
// Set all buttons in same group to inactive
|
||||||
|
Object.keys(allButtons).forEach(btnName => {
|
||||||
|
const btn = allButtons[btnName];
|
||||||
|
const isRadioGroupBtn =
|
||||||
|
btn.config &&
|
||||||
|
btn.config.groupName &&
|
||||||
|
btn.type === 'ohif.radioGroup';
|
||||||
|
|
||||||
|
if (
|
||||||
|
isRadioGroupBtn &&
|
||||||
|
clickedBtn.config.groupName === btn.config.groupName
|
||||||
|
) {
|
||||||
|
btn.props.isActive = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set our clicked button to active
|
||||||
|
allButtons[clickedBtn.id].props.isActive = true;
|
||||||
|
|
||||||
|
// Run button logic/command
|
||||||
|
commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||||
|
|
||||||
|
// Set buttons & trigger notification
|
||||||
|
toolbarService.setButtons(allButtons);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ohif.layoutSelector',
|
||||||
|
defaultComponent: ToolbarLayoutSelector,
|
||||||
|
requiredConfig: [],
|
||||||
|
optionalConfig: [],
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: [],
|
||||||
|
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ohif.toggle',
|
||||||
|
defaultComponent: ToolbarButton,
|
||||||
|
requiredConfig: [],
|
||||||
|
optionalConfig: [],
|
||||||
|
requiredProps: [],
|
||||||
|
optionalProps: [],
|
||||||
|
clickHandler: (evt, clickedBtn, btnSectionName) => {
|
||||||
|
const { props } = clickedBtn;
|
||||||
|
const allButtons = toolbarService.getButtons();
|
||||||
|
const thisButton = allButtons[clickedBtn.id];
|
||||||
|
|
||||||
|
// Set our clicked button to active
|
||||||
|
thisButton.props.isActive = !thisButton.props.isActive;
|
||||||
|
|
||||||
|
// Run button logic/command
|
||||||
|
// MAKE SURE THIS SUPPORTS TOGGLE!
|
||||||
|
// commandsManager.runCommand(props.commandName, props.commandOptions);
|
||||||
|
// What if just toggled "content"?
|
||||||
|
// commandName OR content?
|
||||||
|
|
||||||
|
// Set buttons & trigger notification
|
||||||
|
toolbarService.setButtons(allButtons);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import getCommandsModule from './getCommandsModule.js';
|
|
||||||
import getContextModule from './getContextModule.js';
|
import getContextModule from './getContextModule.js';
|
||||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
@ -12,7 +11,6 @@ export default {
|
|||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
getCommandsModule,
|
|
||||||
getContextModule,
|
getContextModule,
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
// SEE:
|
|
||||||
// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
|
||||||
import React from 'react';
|
|
||||||
import { useViewportGrid } from '@ohif/ui';
|
|
||||||
|
|
||||||
function getCommandsModule({ servicesManager }) {
|
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
const definitions = {
|
|
||||||
toggleLayoutSelectionDialog: {
|
|
||||||
commandFn: () => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
window.alert(
|
|
||||||
'Unable to show dialog; no UI Dialog Service available.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: use SimpleDialog component
|
|
||||||
// TODO: update position on window resize
|
|
||||||
// TODO: Expand service API to check if dialog w/ ID is already open
|
|
||||||
// TODO: Import and call `useViewportGrid`
|
|
||||||
UIDialogService.dismiss({ id: 'layoutSelection' });
|
|
||||||
UIDialogService.create({
|
|
||||||
id: 'layoutSelection',
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: Test,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
context: 'VIEWER',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
definitions,
|
|
||||||
defaultContext: 'VIEWER',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test() {
|
|
||||||
const [
|
|
||||||
{ numCols, numRows, activeViewportIndex, viewports },
|
|
||||||
dispatch,
|
|
||||||
] = useViewportGrid();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_LAYOUT',
|
|
||||||
payload: {
|
|
||||||
numCols: 2,
|
|
||||||
numRows: 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{ color: 'white' }}
|
|
||||||
>
|
|
||||||
Hello World!
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCommandsModule;
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import getCommandsModule from './getCommandsModule.js';
|
|
||||||
import getContextModule from './getContextModule.js';
|
import getContextModule from './getContextModule.js';
|
||||||
import getPanelModule from './getPanelModule.js';
|
import getPanelModule from './getPanelModule.js';
|
||||||
import getViewportModule from './getViewportModule.js';
|
import getViewportModule from './getViewportModule.js';
|
||||||
@ -8,7 +7,6 @@ export default {
|
|||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id: 'org.ohif.measurement-tracking',
|
id: 'org.ohif.measurement-tracking',
|
||||||
getCommandsModule,
|
|
||||||
getContextModule,
|
getContextModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getViewportModule,
|
getViewportModule,
|
||||||
|
|||||||
@ -128,7 +128,9 @@ const definitions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default [];
|
||||||
definitions,
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
// export default {
|
||||||
};
|
// definitions,
|
||||||
|
// defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||||
|
// };
|
||||||
|
|||||||
@ -55,17 +55,17 @@ export default function mode({ modeConfiguration }) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Could import layout selector here from org.ohif.default (when it exists!)
|
// Could import layout selector here from org.ohif.default (when it exists!)
|
||||||
ToolBarService.setToolBarLayout([
|
// ToolBarService.setToolBarLayout([
|
||||||
// Primary
|
// // Primary
|
||||||
{
|
// {
|
||||||
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
// tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
||||||
moreTools: ['Zoom'],
|
// moreTools: ['Zoom'],
|
||||||
},
|
// },
|
||||||
// Secondary
|
// // Secondary
|
||||||
{
|
// {
|
||||||
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
// tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
||||||
},
|
// },
|
||||||
]);
|
// ]);
|
||||||
},
|
},
|
||||||
layoutTemplate: ({ routeProps }) => {
|
layoutTemplate: ({ routeProps }) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import toolbarButtons from './toolbarButtons.js';
|
||||||
|
|
||||||
const ohif = {
|
const ohif = {
|
||||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||||
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
||||||
@ -27,57 +29,24 @@ export default function mode({ modeConfiguration }) {
|
|||||||
init: ({ servicesManager, extensionManager }) => {
|
init: ({ servicesManager, extensionManager }) => {
|
||||||
const { ToolBarService } = servicesManager.services;
|
const { ToolBarService } = servicesManager.services;
|
||||||
ToolBarService.init(extensionManager);
|
ToolBarService.init(extensionManager);
|
||||||
ToolBarService.addButtons([
|
ToolBarService.addButtons(toolbarButtons);
|
||||||
{
|
ToolBarService.createButtonSection('primary', [
|
||||||
id: 'Zoom',
|
'Zoom',
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Zoom',
|
'Wwwc',
|
||||||
},
|
'Pan',
|
||||||
{
|
'Capture',
|
||||||
id: 'Levels',
|
'Layout',
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Wwwc',
|
'Divider',
|
||||||
},
|
['Zoom', 'Wwwc'],
|
||||||
{
|
]);
|
||||||
id: 'Pan',
|
ToolBarService.createButtonSection('secondary', [
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Pan',
|
'Annotate',
|
||||||
},
|
'Bidirectional',
|
||||||
{
|
'Ellipse',
|
||||||
id: 'Capture',
|
'Length',
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Capture',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Layout',
|
|
||||||
namespace: 'org.ohif.default.toolbarModule.Layout',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Annotate',
|
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Annotate',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Bidirectional',
|
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Bidirectional',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Ellipse',
|
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Ellipse',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Length',
|
|
||||||
namespace: 'org.ohif.cornerstone.toolbarModule.Length',
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Could import layout selector here from org.ohif.default (when it exists!)
|
// Could import layout selector here from org.ohif.default (when it exists!)
|
||||||
ToolBarService.setToolBarLayout([
|
|
||||||
// Primary
|
|
||||||
{
|
|
||||||
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
|
|
||||||
moreTools: ['Zoom'],
|
|
||||||
},
|
|
||||||
// Secondary
|
|
||||||
{
|
|
||||||
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
layoutTemplate: ({ routeProps }) => {
|
layoutTemplate: ({ routeProps }) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
132
modes/longitudinal/src/toolbarButtons.js
Normal file
132
modes/longitudinal/src/toolbarButtons.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||||
|
// Only ways that you can pass in a custom React component for render :l
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// Divider
|
||||||
|
{
|
||||||
|
id: 'Divider',
|
||||||
|
type: 'ohif.divider',
|
||||||
|
},
|
||||||
|
// ~~ Primary
|
||||||
|
{
|
||||||
|
id: 'Zoom',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-zoom',
|
||||||
|
label: 'Zoom',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'Zoom' },
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Wwwc',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: true,
|
||||||
|
icon: 'tool-window-level',
|
||||||
|
label: 'Levels',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'Wwwc' },
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Pan',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-move',
|
||||||
|
label: 'Pan',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'Pan' },
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Capture',
|
||||||
|
type: 'ohif.action',
|
||||||
|
props: {
|
||||||
|
icon: 'tool-capture',
|
||||||
|
label: 'Capture',
|
||||||
|
commandName: 'showDownloadViewportModal',
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Layout',
|
||||||
|
type: 'ohif.layoutSelector',
|
||||||
|
},
|
||||||
|
// ~~ Primary: NESTED
|
||||||
|
// ~~ Secondary
|
||||||
|
{
|
||||||
|
id: 'Annotate',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-annotate',
|
||||||
|
label: 'Annotate',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||||
|
type: 'secondary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Bidirectional',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-bidirectional',
|
||||||
|
label: 'Bidirectional',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'Bidirectional' },
|
||||||
|
type: 'secondary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Ellipse',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-elipse',
|
||||||
|
label: 'Ellipse',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'EllipticalRoi' },
|
||||||
|
type: 'secondary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Length',
|
||||||
|
type: 'ohif.radioGroup',
|
||||||
|
config: {
|
||||||
|
groupName: 'primaryTool',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isActive: false,
|
||||||
|
icon: 'tool-length',
|
||||||
|
label: 'Length',
|
||||||
|
commandName: 'setToolActive',
|
||||||
|
commandOptions: { toolName: 'Length' },
|
||||||
|
type: 'secondary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -105,9 +105,6 @@ export default class ExtensionManager {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case MODULE_TYPES.TOOLBAR:
|
case MODULE_TYPES.TOOLBAR:
|
||||||
this._initToolBarModule(extensionModule, extensionId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MODULE_TYPES.VIEWPORT:
|
case MODULE_TYPES.VIEWPORT:
|
||||||
case MODULE_TYPES.PANEL:
|
case MODULE_TYPES.PANEL:
|
||||||
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
case MODULE_TYPES.SOP_CLASS_HANDLER:
|
||||||
@ -115,7 +112,6 @@ export default class ExtensionManager {
|
|||||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||||
// Default for most extension points,
|
// Default for most extension points,
|
||||||
// Just adds each entry ready for consumption by mode.
|
// Just adds each entry ready for consumption by mode.
|
||||||
|
|
||||||
extensionModule.forEach(element => {
|
extensionModule.forEach(element => {
|
||||||
this.modulesMap[
|
this.modulesMap[
|
||||||
`${extensionId}.${moduleType}.${element.name}`
|
`${extensionId}.${moduleType}.${element.name}`
|
||||||
@ -148,27 +144,6 @@ export default class ExtensionManager {
|
|||||||
return this.dataSourceMap[dataSourceName];
|
return this.dataSourceMap[dataSourceName];
|
||||||
};
|
};
|
||||||
|
|
||||||
_initToolBarModule = (extensionModule, extensionId) => {
|
|
||||||
let { definitions, defaultContext } = extensionModule;
|
|
||||||
if (!definitions || Object.keys(definitions).length === 0) {
|
|
||||||
log.warn('Commands Module contains no command definitions');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultContext = defaultContext || 'VIEWER';
|
|
||||||
|
|
||||||
definitions.forEach(definition => {
|
|
||||||
console.log(`${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}`);
|
|
||||||
|
|
||||||
// TODO -> Deep copy instead of mutation? We only do this once, but would be better.
|
|
||||||
definition.context = definition.context || defaultContext;
|
|
||||||
|
|
||||||
this.modulesMap[
|
|
||||||
`${extensionId}.${MODULE_TYPES.TOOLBAR}.${definition.id}`
|
|
||||||
] = definition;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {string} moduleType
|
* @param {string} moduleType
|
||||||
@ -185,13 +160,13 @@ export default class ExtensionManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const extensionModule = getModuleFn({
|
const extensionModule = getModuleFn({
|
||||||
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
|
|
||||||
servicesManager: this._servicesManager,
|
|
||||||
commandsManager: this._commandsManager,
|
|
||||||
appConfig: this._appConfig,
|
appConfig: this._appConfig,
|
||||||
|
getDataSources: this.getDataSources, // Why pass this in if we're passing in `extensionManager`?
|
||||||
|
commandsManager: this._commandsManager,
|
||||||
|
extensionManager: this,
|
||||||
|
servicesManager: this._servicesManager,
|
||||||
configuration,
|
configuration,
|
||||||
api: this._api,
|
api: this._api,
|
||||||
extensionManager: this,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!extensionModule) {
|
if (!extensionModule) {
|
||||||
|
|||||||
@ -6,32 +6,116 @@ const EVENTS = {
|
|||||||
|
|
||||||
export default class ToolBarService {
|
export default class ToolBarService {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.displaySets = {};
|
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
|
this.buttons = {};
|
||||||
|
this.buttonSections = {
|
||||||
|
/**
|
||||||
|
* primary: ['Zoom', 'Wwwc'],
|
||||||
|
* secondary: ['Length', 'RectangleRoi']
|
||||||
|
*/
|
||||||
|
};
|
||||||
|
|
||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
init(extensionManager) {
|
init(extensionManager) {
|
||||||
this.buttons = {};
|
|
||||||
this.extensionManager = extensionManager;
|
this.extensionManager = extensionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
addButtons(buttons) {
|
getButtons() {
|
||||||
buttons.forEach(button => {
|
return this.buttons;
|
||||||
const buttonDefinition = this.extensionManager.getModuleEntry(
|
}
|
||||||
button.namespace
|
|
||||||
|
setButtons(buttons) {
|
||||||
|
this.buttons = buttons;
|
||||||
|
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
_buttonTypes() {
|
||||||
|
console.log(this.extensionManager.modules);
|
||||||
|
const buttonTypes = {};
|
||||||
|
const registeredToolbarModules = this.extensionManager.modules[
|
||||||
|
'toolbarModule'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
Array.isArray(registeredToolbarModules) &&
|
||||||
|
registeredToolbarModules.length
|
||||||
|
) {
|
||||||
|
registeredToolbarModules.forEach(toolbarModule =>
|
||||||
|
toolbarModule.module.forEach(def => {
|
||||||
|
buttonTypes[def.name] = def;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const id = button.id || buttonDefinition.id;
|
return buttonTypes;
|
||||||
|
}
|
||||||
|
|
||||||
this.buttons[id] = buttonDefinition;
|
|
||||||
|
createButtonSection(key, buttons) {
|
||||||
|
// Maybe do this mapping at time of return, instead of time of create
|
||||||
|
// Props check important for validation here...
|
||||||
|
|
||||||
|
this.buttonSections[key] = buttons;
|
||||||
|
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
getButtonSection(key) {
|
||||||
|
const buttonSectionIds = this.buttonSections[key];
|
||||||
|
const buttonsInSection = [];
|
||||||
|
|
||||||
|
if (!buttonSectionIds) {
|
||||||
|
return buttonsInSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonSectionIds.forEach(btnIdOrArray => {
|
||||||
|
const isNested = Array.isArray(btnIdOrArray);
|
||||||
|
|
||||||
|
if (isNested) {
|
||||||
|
const btnIds = btnIdOrArray;
|
||||||
|
const nestedButtons = [];
|
||||||
|
|
||||||
|
btnIds.forEach(nestedBtnId => {
|
||||||
|
const nestedBtn = this.buttons[nestedBtnId];
|
||||||
|
const mappedNestedBtn = this._mapButtonToDisplay(nestedBtn, key);
|
||||||
|
|
||||||
|
nestedButtons.push(mappedNestedBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nestedButtons.length) {
|
||||||
|
buttonsInSection.push(nestedButtons);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const btnId = btnIdOrArray;
|
||||||
|
const btn = this.buttons[btnId];
|
||||||
|
const mappedBtn = this._mapButtonToDisplay(btn, key);
|
||||||
|
|
||||||
|
buttonsInSection.push(mappedBtn);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return buttonsInSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts displaySetService changes.
|
*
|
||||||
|
* @param {object[]} buttons
|
||||||
|
* @param {string} buttons[].id
|
||||||
|
*/
|
||||||
|
addButtons(buttons) {
|
||||||
|
buttons.forEach(button => {
|
||||||
|
if (!this.buttons[button.id]) {
|
||||||
|
this.buttons[button.id] = button;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts toolbarService changes.
|
||||||
*
|
*
|
||||||
* @param {string} eventName The event name
|
* @param {string} eventName The event name
|
||||||
* @return void
|
* @return void
|
||||||
@ -47,33 +131,35 @@ export default class ToolBarService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setToolBarLayout(layouts) {
|
/**
|
||||||
const toolBarLayout = [];
|
*
|
||||||
|
* @param {*} btn
|
||||||
|
* @param {*} btnSection
|
||||||
|
*/
|
||||||
|
_mapButtonToDisplay(btn, btnSection) {
|
||||||
|
const { id, type, component, props } = btn;
|
||||||
|
const buttonType = this._buttonTypes()[type];
|
||||||
|
|
||||||
layouts.forEach(layout => {
|
if (!buttonType) {
|
||||||
const toolBarDefinitions = { tools: [], moreTools: [] };
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { tools, moreTools } = layout;
|
const onClick = evt => {
|
||||||
|
if (buttonType.clickHandler) {
|
||||||
|
buttonType.clickHandler(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
if (btn.props.onClick) {
|
||||||
|
btn.onClick(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
if (btn.props.clickHandler) {
|
||||||
|
btn.clickHandler(evt, btn, btnSection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
tools &&
|
return {
|
||||||
tools.forEach(element => {
|
id,
|
||||||
const button = this.buttons[element];
|
Component: component || buttonType.defaultComponent,
|
||||||
|
componentProps: Object.assign({}, props, { onClick }), //
|
||||||
toolBarDefinitions.tools.push(button);
|
};
|
||||||
});
|
|
||||||
|
|
||||||
moreTools &&
|
|
||||||
moreTools.forEach(element => {
|
|
||||||
const button = this.buttons[element];
|
|
||||||
|
|
||||||
toolBarDefinitions.moreTools.push(button);
|
|
||||||
});
|
|
||||||
|
|
||||||
toolBarLayout.push(toolBarDefinitions);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.toolBarLayout = toolBarLayout;
|
|
||||||
|
|
||||||
this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, toolBarLayout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export {
|
|||||||
InputMultiSelect,
|
InputMultiSelect,
|
||||||
InputText,
|
InputText,
|
||||||
Label,
|
Label,
|
||||||
|
LayoutSelector,
|
||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
Modal,
|
Modal,
|
||||||
NavBar,
|
NavBar,
|
||||||
@ -67,7 +68,6 @@ export {
|
|||||||
ThumbnailNoImage,
|
ThumbnailNoImage,
|
||||||
ThumbnailTracked,
|
ThumbnailTracked,
|
||||||
ThumbnailList,
|
ThumbnailList,
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
80
platform/ui/src/components/LayoutSelector/LayoutSelector.jsx
Normal file
80
platform/ui/src/components/LayoutSelector/LayoutSelector.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// // SEE:
|
||||||
|
// // https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
|
||||||
|
// import React from 'react';
|
||||||
|
// import { useViewportGrid } from '@ohif/ui';
|
||||||
|
|
||||||
|
// function getCommandsModule({ servicesManager }) {
|
||||||
|
// const { UIDialogService } = servicesManager.services;
|
||||||
|
|
||||||
|
// const definitions = {
|
||||||
|
// toggleLayoutSelectionDialog: {
|
||||||
|
// commandFn: () => {
|
||||||
|
// if (!UIDialogService) {
|
||||||
|
// window.alert(
|
||||||
|
// 'Unable to show dialog; no UI Dialog Service available.'
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // TODO: use SimpleDialog component
|
||||||
|
// // TODO: update position on window resize
|
||||||
|
// // TODO: Expand service API to check if dialog w/ ID is already open
|
||||||
|
// // TODO: Import and call `useViewportGrid`
|
||||||
|
// UIDialogService.dismiss({ id: 'layoutSelection' });
|
||||||
|
// UIDialogService.create({
|
||||||
|
// id: 'layoutSelection',
|
||||||
|
// centralize: true,
|
||||||
|
// isDraggable: false,
|
||||||
|
// showOverlay: true,
|
||||||
|
// content: Test,
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
// storeContexts: [],
|
||||||
|
// options: {},
|
||||||
|
// context: 'VIEWER',
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// definitions,
|
||||||
|
// defaultContext: 'VIEWER',
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function Test() {
|
||||||
|
// const [
|
||||||
|
// { numCols, numRows, activeViewportIndex, viewports },
|
||||||
|
// dispatch,
|
||||||
|
// ] = useViewportGrid();
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div
|
||||||
|
// onClick={() => {
|
||||||
|
// dispatch({
|
||||||
|
// type: 'SET_LAYOUT',
|
||||||
|
// payload: {
|
||||||
|
// numCols: 2,
|
||||||
|
// numRows: 2,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }}
|
||||||
|
// style={{ color: 'white' }}
|
||||||
|
// >
|
||||||
|
// Hello World!
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default getCommandsModule;
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function LayoutSelector() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>LAYOUT SELECTOR PLACEHOLDER!</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayoutSelector;
|
||||||
2
platform/ui/src/components/LayoutSelector/index.js
Normal file
2
platform/ui/src/components/LayoutSelector/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import LayoutSelector from './LayoutSelector';
|
||||||
|
export default LayoutSelector;
|
||||||
@ -1,89 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
import { ToolbarButton, IconButton, Icon, Tooltip } from '@ohif/ui';
|
|
||||||
|
|
||||||
const classes = {
|
|
||||||
type: {
|
|
||||||
primary: '',
|
|
||||||
secondary: 'w-full items-center bg-primary-dark px-3',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Toolbar = ({ activeTool, tools, moreTools, type }) => {
|
|
||||||
return (
|
|
||||||
<div className={classnames('flex', classes.type[type])}>
|
|
||||||
{tools.map((tool) => {
|
|
||||||
const { id, onClick, icon, label, dropdownContent } = tool;
|
|
||||||
const isActive = activeTool === tool.id;
|
|
||||||
return (
|
|
||||||
<div className="relative flex justify-center" key={tool.id}>
|
|
||||||
<ToolbarButton
|
|
||||||
id={id}
|
|
||||||
isActive={isActive}
|
|
||||||
onClick={onClick}
|
|
||||||
icon={icon}
|
|
||||||
label={label}
|
|
||||||
dropdownContent={dropdownContent}
|
|
||||||
type={type}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!!moreTools.length && (
|
|
||||||
<>
|
|
||||||
<span className="w-1 border-l h-8 self-center mx-2 border-common-dark" />
|
|
||||||
<Tooltip position="bottom" content="More tools">
|
|
||||||
<IconButton
|
|
||||||
className={classnames(
|
|
||||||
'mx-1 text-common-bright hover:bg-primary-dark hover:text-primary-light'
|
|
||||||
)}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<Icon name="tool-more-menu" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Toolbar.defaultProps = {
|
|
||||||
activeTool: '',
|
|
||||||
moreTools: [],
|
|
||||||
type: 'primary',
|
|
||||||
};
|
|
||||||
|
|
||||||
Toolbar.propTypes = {
|
|
||||||
type: PropTypes.oneOf(['primary', 'secondary']),
|
|
||||||
activeTool: PropTypes.string,
|
|
||||||
tools: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
|
||||||
icon: PropTypes.string,
|
|
||||||
commandName: PropTypes.string,
|
|
||||||
commandOptions: PropTypes.shape({
|
|
||||||
toolName: PropTypes.string,
|
|
||||||
}),
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
dropdownContent: PropTypes.node,
|
|
||||||
})
|
|
||||||
).isRequired,
|
|
||||||
moreTools: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
label: PropTypes.string,
|
|
||||||
icon: PropTypes.string,
|
|
||||||
commandName: PropTypes.string,
|
|
||||||
commandOptions: PropTypes.shape({
|
|
||||||
toolName: PropTypes.string,
|
|
||||||
}),
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Toolbar;
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
---
|
|
||||||
name: Toolbar
|
|
||||||
menu: Data Display
|
|
||||||
route: components/toolbar
|
|
||||||
---
|
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Playground, Props } from 'docz';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { Toolbar, Typography } from '@ohif/ui';
|
|
||||||
|
|
||||||
# Toolbar
|
|
||||||
|
|
||||||
A Toolbar is used to create a list of tools.
|
|
||||||
|
|
||||||
## Import
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Toolbar } from '@ohif/ui';
|
|
||||||
```
|
|
||||||
|
|
||||||
<Playground>
|
|
||||||
{() => {
|
|
||||||
const [activeTool, setActiveTool] = useState('Zoom');
|
|
||||||
const dropdownContent = [
|
|
||||||
{
|
|
||||||
name: 'Soft tissue',
|
|
||||||
value: '400/40',
|
|
||||||
},
|
|
||||||
{ name: 'Lung', value: '1500 / -600' },
|
|
||||||
{ name: 'Liver', value: '150 / 90' },
|
|
||||||
{ name: 'Bone', value: '2500 / 480' },
|
|
||||||
{ name: 'Brain', value: '80 / 40' },
|
|
||||||
];
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
id: 'Zoom',
|
|
||||||
label: 'Zoom',
|
|
||||||
icon: 'tool-zoom',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
onClick: () => setActiveTool('Zoom'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Wwwc',
|
|
||||||
label: 'Levels',
|
|
||||||
icon: 'tool-window-level',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Wwwc' },
|
|
||||||
onClick: () => setActiveTool('Wwwc'),
|
|
||||||
dropdownContent: (
|
|
||||||
<div>
|
|
||||||
{dropdownContent.map((row, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="text-base text-white">{row.name}</span>
|
|
||||||
<span className="text-base text-primary-light ml-3">
|
|
||||||
{row.value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Pan',
|
|
||||||
label: 'Pan',
|
|
||||||
icon: 'tool-move',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Pan' },
|
|
||||||
onClick: () => setActiveTool('Pan'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Capture',
|
|
||||||
label: 'Capture',
|
|
||||||
icon: 'tool-capture',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Capture' },
|
|
||||||
onClick: () => setActiveTool('Capture'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Layout',
|
|
||||||
label: 'Layout',
|
|
||||||
icon: 'tool-layout',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Layout' },
|
|
||||||
onClick: () => setActiveTool('Layout'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const moreTools = [
|
|
||||||
{
|
|
||||||
id: 'Zoom 2',
|
|
||||||
label: 'Zoom',
|
|
||||||
icon: 'tool-zoom',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
onClick: () => setActiveTool('Zoom 2'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Layout 2',
|
|
||||||
label: 'Layout',
|
|
||||||
icon: 'tool-layout',
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Layout' },
|
|
||||||
onClick: () => setActiveTool('Layout 2'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const secondaryTools = [
|
|
||||||
{
|
|
||||||
id: 'Annotate',
|
|
||||||
label: 'Annotate',
|
|
||||||
icon: 'tool-annotate',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Annotate' },
|
|
||||||
onClick: () => console.log('Activate Annotate'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Bidirectional',
|
|
||||||
label: 'Bidirectional',
|
|
||||||
icon: 'tool-bidirectional',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Bidirectional' },
|
|
||||||
onClick: () => console.log('Activate Bidirectional'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Elipse',
|
|
||||||
label: 'Elipse',
|
|
||||||
icon: 'tool-elipse',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Elipse' },
|
|
||||||
onClick: () => console.log('Activate Elipse'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Length',
|
|
||||||
label: 'Length',
|
|
||||||
icon: 'tool-length',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Length' },
|
|
||||||
onClick: () => console.log('Activate Length'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<div className="py-10 px-4 flex flex-col justify-center">
|
|
||||||
<div className="mb-4">
|
|
||||||
<Typography className="mb-2">Primary Toolbar</Typography>
|
|
||||||
<Toolbar
|
|
||||||
moreTools={moreTools}
|
|
||||||
activeTool={activeTool}
|
|
||||||
tools={tools}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography className="mb-2">Secondary Toolbar</Typography>
|
|
||||||
<Toolbar tools={secondaryTools} type="secondary" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Playground>
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
<Props of={Toolbar} />
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
import Toolbar from './Toolbar';
|
|
||||||
export default Toolbar;
|
|
||||||
@ -28,6 +28,7 @@ const ToolbarButton = ({
|
|||||||
return (
|
return (
|
||||||
<div key={id}>
|
<div key={id}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
isSticky={shouldShowDropdown}
|
||||||
content={shouldShowDropdown ? dropdownContent : label}
|
content={shouldShowDropdown ? dropdownContent : label}
|
||||||
tight={shouldShowDropdown}
|
tight={shouldShowDropdown}
|
||||||
>
|
>
|
||||||
@ -51,6 +52,7 @@ ToolbarButton.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ToolbarButton.propTypes = {
|
ToolbarButton.propTypes = {
|
||||||
|
/* Influences background/hover styling */
|
||||||
type: PropTypes.oneOf(['primary', 'secondary']),
|
type: PropTypes.oneOf(['primary', 'secondary']),
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
@ -58,7 +60,7 @@ ToolbarButton.propTypes = {
|
|||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
|
/** Tooltip content can be replaced for a customized content by passing a node to this value. */
|
||||||
dropdownContent: PropTypes.node,
|
dropdownContent: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ToolbarButton;
|
export default ToolbarButton;
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const arrowPositionStyle = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tooltip = ({ position, content, tight, children }) => {
|
const Tooltip = ({ content, isSticky, position, tight, children }) => {
|
||||||
const [isActive, setIsActive] = useState(false);
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
|
||||||
const handleMouseOver = () => {
|
const handleMouseOver = () => {
|
||||||
@ -39,6 +39,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOpen = isSticky || isActive;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative "
|
className="relative "
|
||||||
@ -51,8 +53,8 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
{children}
|
{children}
|
||||||
<div
|
<div
|
||||||
className={classnames(`tooltip tooltip-${position}`, {
|
className={classnames(`tooltip tooltip-${position}`, {
|
||||||
block: isActive,
|
block: isOpen,
|
||||||
hidden: !isActive,
|
hidden: !isOpen,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -63,9 +65,9 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{content}
|
{typeof content === 'function' ? content() : content}
|
||||||
<svg
|
<svg
|
||||||
className="absolute text-primary-dark h-4 stroke-secondary-main"
|
className="absolute h-4 text-primary-dark stroke-secondary-main"
|
||||||
style={arrowPositionStyle[position]}
|
style={arrowPositionStyle[position]}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -80,11 +82,12 @@ const Tooltip = ({ position, content, tight, children }) => {
|
|||||||
|
|
||||||
Tooltip.defaultProps = {
|
Tooltip.defaultProps = {
|
||||||
tight: false,
|
tight: false,
|
||||||
|
isSticky: false,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
tight: PropTypes.bool,
|
content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
position: PropTypes.oneOf([
|
position: PropTypes.oneOf([
|
||||||
'bottom',
|
'bottom',
|
||||||
'bottom-left',
|
'bottom-left',
|
||||||
@ -92,8 +95,9 @@ Tooltip.propTypes = {
|
|||||||
'left',
|
'left',
|
||||||
'right',
|
'right',
|
||||||
]),
|
]),
|
||||||
|
isSticky: PropTypes.bool,
|
||||||
|
tight: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
content: PropTypes.node.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Tooltip;
|
export default Tooltip;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
@apply z-10 absolute;
|
@apply absolute z-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
/* TOOLTIP WORKAROUND FOR ARROW UP */
|
||||||
.tooltip.tooltip-bottom .tooltip-box::before {
|
.tooltip.tooltip-bottom .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-right .tooltip-box::before {
|
.tooltip.tooltip-right .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
@ -54,7 +54,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-left .tooltip-box::before {
|
.tooltip.tooltip-left .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
.tooltip.tooltip-bottom-right .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@ -73,19 +73,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
.tooltip.tooltip-bottom-left .tooltip-box::before {
|
||||||
@apply bg-primary-dark absolute z-10;
|
@apply absolute z-10 bg-primary-dark;
|
||||||
content: '';
|
content: '';
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
left: 5px;
|
left: 5px;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: REMOVE BELOW CLASS "showTooltipOnHover" AFTER TOOLTIP REPLACEMENT */
|
|
||||||
.showTooltipOnHover .tooltip {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.showTooltipOnHover:hover .tooltip {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -184,7 +184,7 @@ const ViewportActionBar = ({ studyData, onSeriesChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="relative flex justify-end showTooltipOnHover">
|
<div className="relative flex justify-end">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Icon name="profile" className="w-5 text-white" />
|
<Icon name="profile" className="w-5 text-white" />
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import InputLabelWrapper from './InputLabelWrapper';
|
|||||||
import InputMultiSelect from './InputMultiSelect';
|
import InputMultiSelect from './InputMultiSelect';
|
||||||
import InputText from './InputText';
|
import InputText from './InputText';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
import LayoutSelector from './LayoutSelector';
|
||||||
import MeasurementTable from './MeasurementTable';
|
import MeasurementTable from './MeasurementTable';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import NavBar from './NavBar';
|
import NavBar from './NavBar';
|
||||||
@ -37,7 +38,6 @@ import Thumbnail from './Thumbnail';
|
|||||||
import ThumbnailNoImage from './ThumbnailNoImage';
|
import ThumbnailNoImage from './ThumbnailNoImage';
|
||||||
import ThumbnailTracked from './ThumbnailTracked';
|
import ThumbnailTracked from './ThumbnailTracked';
|
||||||
import ThumbnailList from './ThumbnailList';
|
import ThumbnailList from './ThumbnailList';
|
||||||
import Toolbar from './Toolbar';
|
|
||||||
import ToolbarButton from './ToolbarButton';
|
import ToolbarButton from './ToolbarButton';
|
||||||
import Tooltip from './Tooltip';
|
import Tooltip from './Tooltip';
|
||||||
import Typography from './Typography';
|
import Typography from './Typography';
|
||||||
@ -61,6 +61,7 @@ export {
|
|||||||
InputMultiSelect,
|
InputMultiSelect,
|
||||||
InputText,
|
InputText,
|
||||||
Label,
|
Label,
|
||||||
|
LayoutSelector,
|
||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
Modal,
|
Modal,
|
||||||
NavBar,
|
NavBar,
|
||||||
@ -87,7 +88,6 @@ export {
|
|||||||
ThumbnailNoImage,
|
ThumbnailNoImage,
|
||||||
ThumbnailTracked,
|
ThumbnailTracked,
|
||||||
ThumbnailList,
|
ThumbnailList,
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
|
|||||||
@ -1,14 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { SidePanel, StudyBrowser, DragAndDropProvider } from '@ohif/ui';
|
||||||
SidePanel,
|
|
||||||
StudyBrowser,
|
|
||||||
DragAndDropProvider,
|
|
||||||
ViewportGrid,
|
|
||||||
ViewportPane,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
|
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import ViewportToolbar from './components/ViewportToolBar';
|
|
||||||
|
|
||||||
const Viewer = () => {
|
const Viewer = () => {
|
||||||
return (
|
return (
|
||||||
@ -16,7 +9,7 @@ const Viewer = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Header />
|
<Header />
|
||||||
<div
|
<div
|
||||||
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
|
className="flex flex-row flex-no-wrap items-stretch flex-1 w-full overflow-hidden"
|
||||||
style={{ height: 'calc(100vh - 57px' }}
|
style={{ height: 'calc(100vh - 57px' }}
|
||||||
>
|
>
|
||||||
<SidePanel
|
<SidePanel
|
||||||
@ -28,8 +21,8 @@ const Viewer = () => {
|
|||||||
>
|
>
|
||||||
<StudyBrowser />
|
<StudyBrowser />
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
|
<div className="flex items-center justify-center flex-1 overflow-hidden text-white h-100 bg-primary-main">
|
||||||
<ViewportToolbar />
|
{/* <ViewportToolbar /> */}
|
||||||
<div>CONTENT</div>
|
<div>CONTENT</div>
|
||||||
</div>
|
</div>
|
||||||
<SidePanel
|
<SidePanel
|
||||||
@ -39,7 +32,7 @@ const Viewer = () => {
|
|||||||
componentLabel="Measurements"
|
componentLabel="Measurements"
|
||||||
defaultIsOpen={false}
|
defaultIsOpen={false}
|
||||||
>
|
>
|
||||||
<div className="flex justify-center text-white p-2">
|
<div className="flex justify-center p-2 text-white">
|
||||||
panel placeholder
|
panel placeholder
|
||||||
</div>
|
</div>
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import {
|
|||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import ViewportToolbar from './components/ViewportToolBar';
|
|
||||||
|
|
||||||
import { tabs } from './studyBrowserMockData';
|
import { tabs } from './studyBrowserMockData';
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ import { tabs } from './studyBrowserMockData';
|
|||||||
{/* TOOLBAR + GRID */}
|
{/* TOOLBAR + GRID */}
|
||||||
<div className="flex flex-col flex-1 h-full">
|
<div className="flex flex-col flex-1 h-full">
|
||||||
<div className="flex flex-2 w-100 border-b border-transparent h-12">
|
<div className="flex flex-2 w-100 border-b border-transparent h-12">
|
||||||
<ViewportToolbar />
|
{/* <ViewportToolbar /> Secondary Toolbar */}
|
||||||
</div>
|
</div>
|
||||||
{/* VIEWPORT GRID CONTAINER */}
|
{/* VIEWPORT GRID CONTAINER */}
|
||||||
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
|
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { NavBar, Svg, Icon, IconButton, Toolbar } from '@ohif/ui';
|
import { NavBar, Svg, Icon, IconButton } from '@ohif/ui';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [activeTool, setActiveTool] = useState('Zoom');
|
const [activeTool, setActiveTool] = useState('Zoom');
|
||||||
@ -34,15 +34,15 @@ const Header = () => {
|
|||||||
{dropdownContent.map((row, i) => (
|
{dropdownContent.map((row, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex justify-between py-2 px-3 hover:bg-secondary-dark cursor-pointer"
|
className="flex justify-between px-3 py-2 cursor-pointer hover:bg-secondary-dark"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-base text-white">{row.name}</span>
|
<span className="text-base text-white">{row.name}</span>
|
||||||
<span className="text-base text-primary-light ml-3">
|
<span className="ml-3 text-base text-primary-light">
|
||||||
{row.value}
|
{row.value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base text-primary-active ml-4">{i}</span>
|
<span className="ml-4 text-base text-primary-active">{i}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -75,12 +75,12 @@ const Header = () => {
|
|||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<NavBar className="justify-between border-b-4 border-black">
|
<NavBar className="justify-between border-b-4 border-black">
|
||||||
<div className="flex flex-1 justify-between">
|
<div className="flex justify-between flex-1">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="mr-3 inline-flex items-center">
|
<div className="inline-flex items-center mr-3">
|
||||||
<Icon
|
<Icon
|
||||||
name="chevron-left"
|
name="chevron-left"
|
||||||
className="text-primary-active w-8 cursor-pointer"
|
className="w-8 cursor-pointer text-primary-active"
|
||||||
onClick={() => alert('Navigate to previous page')}
|
onClick={() => alert('Navigate to previous page')}
|
||||||
/>
|
/>
|
||||||
<a href="#" className="ml-4">
|
<a href="#" className="ml-4">
|
||||||
@ -89,10 +89,10 @@ const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Toolbar tools={tools} activeTool={activeTool} moreTools={tools} />
|
{/* <Toolbar tools={tools} activeTool={activeTool} moreTools={tools} /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-3 text-common-light text-lg">
|
<span className="mr-3 text-lg text-common-light">
|
||||||
FOR INVESTIGATIONAL USE ONLY
|
FOR INVESTIGATIONAL USE ONLY
|
||||||
</span>
|
</span>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@ -1,47 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Toolbar } from '@ohif/ui';
|
|
||||||
|
|
||||||
const ViewportToolbar = () => {
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
id: 'Annotate',
|
|
||||||
label: 'Annotate',
|
|
||||||
icon: 'tool-annotate',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Annotate' },
|
|
||||||
onClick: () => console.log('Activate Annotate'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Bidirectional',
|
|
||||||
label: 'Bidirectional',
|
|
||||||
icon: 'tool-bidirectional',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Bidirectional' },
|
|
||||||
onClick: () => console.log('Activate Bidirectional'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Elipse',
|
|
||||||
label: 'Elipse',
|
|
||||||
icon: 'tool-elipse',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Elipse' },
|
|
||||||
onClick: () => console.log('Activate Elipse'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Length',
|
|
||||||
label: 'Length',
|
|
||||||
icon: 'tool-length',
|
|
||||||
type: null,
|
|
||||||
commandName: 'setToolActive',
|
|
||||||
commandOptions: { toolName: 'Length' },
|
|
||||||
onClick: () => console.log('Activate Length'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return <Toolbar type="secondary" tools={tools} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewportToolbar;
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { StudyBrowser } from '@ohif/ui';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
|
||||||
|
|
||||||
const { setActiveViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData)
|
|
||||||
// - Pass in errors and stack loading progress from Redux
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
// If we know that the stack loading progress details have changed,
|
|
||||||
// we can try to update the component state so that the thumbnail
|
|
||||||
// progress bar is updated
|
|
||||||
const stackLoadingProgressMap = state.loading.progress;
|
|
||||||
const studiesWithLoadingData = cloneDeep(ownProps.studies);
|
|
||||||
|
|
||||||
studiesWithLoadingData.forEach(study => {
|
|
||||||
study.thumbnails.forEach(data => {
|
|
||||||
const { displaySetInstanceUID } = data;
|
|
||||||
const stackId = `StackProgress:${displaySetInstanceUID}`;
|
|
||||||
const stackProgressData = stackLoadingProgressMap[stackId];
|
|
||||||
|
|
||||||
let stackPercentComplete = 0;
|
|
||||||
if (stackProgressData) {
|
|
||||||
stackPercentComplete = stackProgressData.percentComplete;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.stackPercentComplete = stackPercentComplete;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
studies: studiesWithLoadingData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
|
||||||
return {
|
|
||||||
onThumbnailClick: displaySetInstanceUID => {
|
|
||||||
const displaySet = findDisplaySetByUID(
|
|
||||||
ownProps.studyMetadata,
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(setActiveViewportSpecificData(displaySet));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedStudyBrowser = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(StudyBrowser);
|
|
||||||
|
|
||||||
export default ConnectedStudyBrowser;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import ViewerMain from './ViewerMain';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
setViewportSpecificData,
|
|
||||||
clearViewportSpecificData,
|
|
||||||
} = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeViewportIndex,
|
|
||||||
layout,
|
|
||||||
viewportSpecificData,
|
|
||||||
viewports: state.viewports,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
setViewportSpecificData: (viewportIndex, data) => {
|
|
||||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
|
||||||
},
|
|
||||||
clearViewportSpecificData: () => {
|
|
||||||
dispatch(clearViewportSpecificData());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedViewerMain = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ViewerMain);
|
|
||||||
|
|
||||||
export default ConnectedViewerMain;
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
|
|
||||||
const { clearViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
const activeServer = state.servers.servers.find(isActive);
|
|
||||||
|
|
||||||
return {
|
|
||||||
server: ownProps.server || activeServer,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
clearViewportSpecificData: () => {
|
|
||||||
dispatch(clearViewportSpecificData());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedViewerRetrieveStudyData = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ViewerRetrieveStudyData);
|
|
||||||
|
|
||||||
export default ConnectedViewerRetrieveStudyData;
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { ContextMenu } from '@ohif/ui';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { commandsManager } from './../App.jsx';
|
|
||||||
|
|
||||||
const toolTypes = [
|
|
||||||
'Angle',
|
|
||||||
'Bidirectional',
|
|
||||||
'Length',
|
|
||||||
'FreehandMouse',
|
|
||||||
'EllipticalRoi',
|
|
||||||
'CircleRoi',
|
|
||||||
'RectangleRoi',
|
|
||||||
];
|
|
||||||
|
|
||||||
const ToolContextMenu = ({
|
|
||||||
onSetLabel,
|
|
||||||
onSetDescription,
|
|
||||||
isTouchEvent,
|
|
||||||
eventData,
|
|
||||||
onClose,
|
|
||||||
onDelete,
|
|
||||||
}) => {
|
|
||||||
const defaultDropdownItems = [
|
|
||||||
{
|
|
||||||
label: 'Delete measurement',
|
|
||||||
actionType: 'Delete',
|
|
||||||
action: ({ nearbyToolData, eventData }) =>
|
|
||||||
onDelete(nearbyToolData, eventData),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Relabel',
|
|
||||||
actionType: 'setLabel',
|
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
|
||||||
const { tool: measurementData } = nearbyToolData;
|
|
||||||
onSetLabel(eventData, measurementData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actionType: 'setDescription',
|
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
|
||||||
const { tool: measurementData } = nearbyToolData;
|
|
||||||
onSetDescription(eventData, measurementData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getDropdownItems = (eventData, isTouchEvent = false) => {
|
|
||||||
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
|
||||||
element: eventData.element,
|
|
||||||
canvasCoordinates: eventData.currentPoints.canvas,
|
|
||||||
availableToolTypes: toolTypes,
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Annotate tools for touch events already have a press handle to edit it,
|
|
||||||
* has a better UX for deleting it.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
isTouchEvent &&
|
|
||||||
nearbyToolData &&
|
|
||||||
nearbyToolData.toolType === 'arrowAnnotate'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropdownItems = [];
|
|
||||||
if (nearbyToolData) {
|
|
||||||
defaultDropdownItems.forEach(item => {
|
|
||||||
item.params = { eventData, nearbyToolData };
|
|
||||||
|
|
||||||
if (item.actionType === 'setDescription') {
|
|
||||||
item.label = `${
|
|
||||||
nearbyToolData.tool.description ? 'Edit' : 'Add'
|
|
||||||
} Description`;
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdownItems.push(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropdownItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickHandler = ({ action, params }) => {
|
|
||||||
action(params);
|
|
||||||
if (onClose) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ToolContextMenu">
|
|
||||||
<ContextMenu items={dropdownItems} onClick={onClickHandler} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolContextMenu.propTypes = {
|
|
||||||
isTouchEvent: PropTypes.bool.isRequired,
|
|
||||||
eventData: PropTypes.object,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
onSetDescription: PropTypes.func,
|
|
||||||
onSetLabel: PropTypes.func,
|
|
||||||
onDelete: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolContextMenu.defaultProps = {
|
|
||||||
isTouchEvent: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ToolContextMenu;
|
|
||||||
@ -1,386 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
|
||||||
import {
|
|
||||||
ExpandableToolMenu,
|
|
||||||
RoundedButtonGroup,
|
|
||||||
ToolbarButton,
|
|
||||||
withModal,
|
|
||||||
withDialog,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
|
|
||||||
import './ToolbarRow.css';
|
|
||||||
import { commandsManager, extensionManager } from './../App.jsx';
|
|
||||||
|
|
||||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
|
||||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
|
||||||
import { withAppContext } from '../context/AppContext';
|
|
||||||
|
|
||||||
class ToolbarRow extends Component {
|
|
||||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
|
||||||
// closed if selected is null/undefined
|
|
||||||
static propTypes = {
|
|
||||||
isLeftSidePanelOpen: PropTypes.bool.isRequired,
|
|
||||||
isRightSidePanelOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedLeftSidePanel: PropTypes.string.isRequired,
|
|
||||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
|
||||||
handleSidePanelChange: PropTypes.func.isRequired,
|
|
||||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
studies: PropTypes.array,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
// NOTE: withDialog, withModal HOCs
|
|
||||||
dialog: PropTypes.any,
|
|
||||||
modal: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
studies: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this);
|
|
||||||
// TODO:
|
|
||||||
// If it's a tool that can be active... Mark it as active?
|
|
||||||
// - Tools that are on/off?
|
|
||||||
// - Tools that can be bound to multiple buttons?
|
|
||||||
|
|
||||||
// Normal ToolbarButtons...
|
|
||||||
// Just how high do we need to hoist this state?
|
|
||||||
// Why ToolbarRow instead of just Toolbar? Do we have any others?
|
|
||||||
this.state = {
|
|
||||||
toolbarButtons: toolbarButtonDefinitions,
|
|
||||||
activeButtons: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.seriesPerStudyCount = [];
|
|
||||||
|
|
||||||
this._handleBuiltIn = _handleBuiltIn.bind(this);
|
|
||||||
|
|
||||||
this.updateButtonGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateButtonGroups() {
|
|
||||||
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
|
|
||||||
|
|
||||||
this.buttonGroups = {
|
|
||||||
left: [],
|
|
||||||
right: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// ~ FIND MENU OPTIONS
|
|
||||||
panelModules.forEach(panelExtension => {
|
|
||||||
const panelModule = panelExtension.module;
|
|
||||||
const defaultContexts = Array.from(panelModule.defaultContext);
|
|
||||||
|
|
||||||
panelModule.menuOptions.forEach(menuOption => {
|
|
||||||
const contexts = Array.from(menuOption.context || defaultContexts);
|
|
||||||
const hasActiveContext = this.props.activeContexts.some(actx =>
|
|
||||||
contexts.includes(actx)
|
|
||||||
);
|
|
||||||
|
|
||||||
// It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport?
|
|
||||||
// Note: This does not cleanly handle `studies` prop updating with panel open
|
|
||||||
const isDisabled =
|
|
||||||
typeof menuOption.isDisabled === 'function' &&
|
|
||||||
menuOption.isDisabled(this.props.studies);
|
|
||||||
|
|
||||||
if (hasActiveContext && !isDisabled) {
|
|
||||||
const menuOptionEntry = {
|
|
||||||
value: menuOption.target,
|
|
||||||
icon: menuOption.icon,
|
|
||||||
bottomLabel: menuOption.label,
|
|
||||||
};
|
|
||||||
const from = menuOption.from || 'right';
|
|
||||||
|
|
||||||
this.buttonGroups[from].push(menuOptionEntry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This should come from extensions, instead of being baked in
|
|
||||||
this.buttonGroups.left.unshift({
|
|
||||||
value: 'studies',
|
|
||||||
icon: 'th-large',
|
|
||||||
bottomLabel: this.props.t('Series'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const activeContextsChanged =
|
|
||||||
prevProps.activeContexts !== this.props.activeContexts;
|
|
||||||
|
|
||||||
const prevStudies = prevProps.studies;
|
|
||||||
const studies = this.props.studies;
|
|
||||||
const seriesPerStudyCount = this.seriesPerStudyCount;
|
|
||||||
|
|
||||||
let studiesUpdated = false;
|
|
||||||
|
|
||||||
if (prevStudies.length !== studies.length) {
|
|
||||||
studiesUpdated = true;
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < studies.length; i++) {
|
|
||||||
if (studies[i].series.length !== seriesPerStudyCount[i]) {
|
|
||||||
seriesPerStudyCount[i] = studies[i].series.length;
|
|
||||||
|
|
||||||
studiesUpdated = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (studiesUpdated) {
|
|
||||||
this.updateButtonGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeContextsChanged) {
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
toolbarButtons: _getVisibleToolbarButtons.call(this),
|
|
||||||
},
|
|
||||||
this.closeCineDialogIfNotApplicable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCineDialogIfNotApplicable = () => {
|
|
||||||
const { dialog } = this.props;
|
|
||||||
let { dialogId, activeButtons, toolbarButtons } = this.state;
|
|
||||||
if (dialogId) {
|
|
||||||
const cineButtonPresent = toolbarButtons.find(
|
|
||||||
button => button.options && button.options.behavior === 'CINE'
|
|
||||||
);
|
|
||||||
if (!cineButtonPresent) {
|
|
||||||
dialog.dismiss({ id: dialogId });
|
|
||||||
activeButtons = activeButtons.filter(
|
|
||||||
button => button.options && button.options.behavior !== 'CINE'
|
|
||||||
);
|
|
||||||
this.setState({ dialogId: null, activeButtons });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const buttonComponents = _getButtonComponents.call(
|
|
||||||
this,
|
|
||||||
this.state.toolbarButtons,
|
|
||||||
this.state.activeButtons
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPress = (side, value) => {
|
|
||||||
this.props.handleSidePanelChange(side, value);
|
|
||||||
};
|
|
||||||
const onPressLeft = onPress.bind(this, 'left');
|
|
||||||
const onPressRight = onPress.bind(this, 'right');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="ToolbarRow">
|
|
||||||
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
|
|
||||||
<RoundedButtonGroup
|
|
||||||
options={this.buttonGroups.left}
|
|
||||||
value={this.props.selectedLeftSidePanel || ''}
|
|
||||||
onValueChanged={onPressLeft}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{buttonComponents}
|
|
||||||
<ConnectedLayoutButton />
|
|
||||||
<div
|
|
||||||
className="pull-right m-t-1 rm-x-1"
|
|
||||||
style={{ marginLeft: 'auto' }}
|
|
||||||
>
|
|
||||||
{this.buttonGroups.right.length && (
|
|
||||||
<RoundedButtonGroup
|
|
||||||
options={this.buttonGroups.right}
|
|
||||||
value={this.props.selectedRightSidePanel || ''}
|
|
||||||
onValueChanged={onPressRight}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getCustomButtonComponent(button, activeButtons) {
|
|
||||||
const CustomComponent = button.CustomComponent;
|
|
||||||
const isValidComponent = typeof CustomComponent === 'function';
|
|
||||||
|
|
||||||
// Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented.
|
|
||||||
if (isValidComponent) {
|
|
||||||
const parentContext = this;
|
|
||||||
const activeButtonsIds = activeButtons.map(button => button.id);
|
|
||||||
const isActive = activeButtonsIds.includes(button.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomComponent
|
|
||||||
parentContext={parentContext}
|
|
||||||
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
|
|
||||||
button={button}
|
|
||||||
key={button.id}
|
|
||||||
activeButtons={activeButtonsIds}
|
|
||||||
isActive={isActive}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getExpandableButtonComponent(button, activeButtons) {
|
|
||||||
// Iterate over button definitions and update `onClick` behavior
|
|
||||||
let activeCommand;
|
|
||||||
const childButtons = button.buttons.map(childButton => {
|
|
||||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
|
||||||
|
|
||||||
if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) {
|
|
||||||
activeCommand = childButton.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return childButton;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExpandableToolMenu
|
|
||||||
key={button.id}
|
|
||||||
label={button.label}
|
|
||||||
icon={button.icon}
|
|
||||||
buttons={childButtons}
|
|
||||||
activeCommand={activeCommand}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getDefaultButtonComponent(button, activeButtons) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
key={button.id}
|
|
||||||
label={button.label}
|
|
||||||
icon={button.icon}
|
|
||||||
onClick={_handleToolbarButtonClick.bind(this, button)}
|
|
||||||
isActive={activeButtons.map(button => button.id).includes(button.id)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Determine which extension buttons should be showing, if they're
|
|
||||||
* active, and what their onClick behavior should be.
|
|
||||||
*/
|
|
||||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
|
||||||
const _this = this;
|
|
||||||
return toolbarButtons.map(button => {
|
|
||||||
const hasCustomComponent = button.CustomComponent;
|
|
||||||
const hasNestedButtonDefinitions = button.buttons && button.buttons.length;
|
|
||||||
|
|
||||||
if (hasCustomComponent) {
|
|
||||||
return _getCustomButtonComponent.call(_this, button, activeButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNestedButtonDefinitions) {
|
|
||||||
return _getExpandableButtonComponent.call(_this, button, activeButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _getDefaultButtonComponent.call(_this, button, activeButtons);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: DEPRECATE
|
|
||||||
* This is used exclusively in `extensions/cornerstone/src`
|
|
||||||
* We have better ways with new UI Services to trigger "builtin" behaviors
|
|
||||||
*
|
|
||||||
* A handy way for us to handle different button types. IE. firing commands for
|
|
||||||
* buttons, or initiation built in behavior.
|
|
||||||
*
|
|
||||||
* @param {*} button
|
|
||||||
* @param {*} evt
|
|
||||||
* @param {*} props
|
|
||||||
*/
|
|
||||||
function _handleToolbarButtonClick(button, evt, props) {
|
|
||||||
const { activeButtons } = this.state;
|
|
||||||
|
|
||||||
if (button.commandName) {
|
|
||||||
const options = Object.assign({ evt }, button.commandOptions);
|
|
||||||
commandsManager.runCommand(button.commandName, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use Types ENUM
|
|
||||||
// TODO: We can update this to be a `getter` on the extension to query
|
|
||||||
// For the active tools after we apply our updates?
|
|
||||||
if (button.type === 'setToolActive') {
|
|
||||||
const toggables = activeButtons.filter(
|
|
||||||
({ options }) => options && !options.togglable
|
|
||||||
);
|
|
||||||
this.setState({ activeButtons: [...toggables, button] });
|
|
||||||
} else if (button.type === 'builtIn') {
|
|
||||||
this._handleBuiltIn(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function _getVisibleToolbarButtons() {
|
|
||||||
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
|
|
||||||
const toolbarButtonDefinitions = [];
|
|
||||||
|
|
||||||
toolbarModules.forEach(extension => {
|
|
||||||
const { definitions, defaultContext } = extension.module;
|
|
||||||
definitions.forEach(definition => {
|
|
||||||
const context = definition.context || defaultContext;
|
|
||||||
|
|
||||||
if (this.props.activeContexts.includes(context)) {
|
|
||||||
toolbarButtonDefinitions.push(definition);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return toolbarButtonDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleBuiltIn(button) {
|
|
||||||
/* TODO: Keep cine button active until its unselected. */
|
|
||||||
const { dialog, t } = this.props;
|
|
||||||
const { dialogId } = this.state;
|
|
||||||
const { id, options } = button;
|
|
||||||
|
|
||||||
if (options.behavior === 'CINE') {
|
|
||||||
if (dialogId) {
|
|
||||||
dialog.dismiss({ id: dialogId });
|
|
||||||
this.setState(state => ({
|
|
||||||
dialogId: null,
|
|
||||||
activeButtons: [
|
|
||||||
...state.activeButtons.filter(button => button.id !== id),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const spacing = 20;
|
|
||||||
const { x, y } = document
|
|
||||||
.querySelector(`.ViewerMain`)
|
|
||||||
.getBoundingClientRect();
|
|
||||||
const newDialogId = dialog.create({
|
|
||||||
content: ConnectedCineDialog,
|
|
||||||
defaultPosition: {
|
|
||||||
x: x + spacing || 0,
|
|
||||||
y: y + spacing || 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.setState(state => ({
|
|
||||||
dialogId: newDialogId,
|
|
||||||
activeButtons: [...state.activeButtons, button],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
|
|
||||||
commandsManager.runCommand('showDownloadViewportModal', {
|
|
||||||
title: t('Download High Quality Image'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
|
||||||
withModal(withDialog(withAppContext(ToolbarRow)))
|
|
||||||
);
|
|
||||||
@ -1,400 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
|
||||||
import OHIF, { DICOMSR } from '@ohif/core';
|
|
||||||
import { withDialog } from '@ohif/ui';
|
|
||||||
import moment from 'moment';
|
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
|
||||||
import ToolbarRow from './ToolbarRow.js';
|
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|
||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
|
||||||
import SidePanel from './../components/SidePanel.js';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
|
||||||
import UserManagerContext from '../context/UserManagerContext';
|
|
||||||
import AppContext from '../context/AppContext';
|
|
||||||
|
|
||||||
import './Viewer.css';
|
|
||||||
|
|
||||||
class Viewer extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
StudyInstanceUID: PropTypes.string.isRequired,
|
|
||||||
StudyDate: PropTypes.string,
|
|
||||||
displaySets: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
|
||||||
SeriesDescription: PropTypes.string,
|
|
||||||
SeriesNumber: PropTypes.number,
|
|
||||||
InstanceNumber: PropTypes.number,
|
|
||||||
numImageFrames: PropTypes.number,
|
|
||||||
Modality: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
getImageId: PropTypes.func.isRequired,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
studyInstanceUIDs: PropTypes.array,
|
|
||||||
activeServer: PropTypes.shape({
|
|
||||||
type: PropTypes.string,
|
|
||||||
wadoRoot: PropTypes.string,
|
|
||||||
}),
|
|
||||||
onTimepointsUpdated: PropTypes.func,
|
|
||||||
onMeasurementsUpdated: PropTypes.func,
|
|
||||||
// window.store.getState().viewports.viewportSpecificData
|
|
||||||
viewports: PropTypes.object.isRequired,
|
|
||||||
// window.store.getState().viewports.activeViewportIndex
|
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
|
||||||
isStudyLoaded: PropTypes.bool,
|
|
||||||
dialog: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { activeServer } = this.props;
|
|
||||||
const server = Object.assign({}, activeServer);
|
|
||||||
|
|
||||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: DICOMSR.retrieveMeasurements,
|
|
||||||
store: DICOMSR.storeMeasurements,
|
|
||||||
},
|
|
||||||
server,
|
|
||||||
});
|
|
||||||
|
|
||||||
OHIF.measurements.TimepointApi.setConfiguration({
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: this.retrieveTimepoints,
|
|
||||||
store: this.storeTimepoints,
|
|
||||||
remove: this.removeTimepoint,
|
|
||||||
update: this.updateTimepoint,
|
|
||||||
disassociate: this.disassociateStudy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
isLeftSidePanelOpen: true,
|
|
||||||
isRightSidePanelOpen: false,
|
|
||||||
selectedRightSidePanel: '',
|
|
||||||
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
|
||||||
thumbnails: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.props.dialog) {
|
|
||||||
this.props.dialog.dismissAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveTimepoints = filter => {
|
|
||||||
OHIF.log.info('retrieveTimepoints');
|
|
||||||
|
|
||||||
// Get the earliest and latest study date
|
|
||||||
let earliestDate = new Date().toISOString();
|
|
||||||
let latestDate = new Date().toISOString();
|
|
||||||
if (this.props.studies) {
|
|
||||||
latestDate = new Date('1000-01-01').toISOString();
|
|
||||||
this.props.studies.forEach(study => {
|
|
||||||
const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString();
|
|
||||||
if (StudyDate < earliestDate) {
|
|
||||||
earliestDate = StudyDate;
|
|
||||||
}
|
|
||||||
if (StudyDate > latestDate) {
|
|
||||||
latestDate = StudyDate;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a generic timepoint
|
|
||||||
return Promise.resolve([
|
|
||||||
{
|
|
||||||
timepointType: 'baseline',
|
|
||||||
timepointId: 'TimepointId',
|
|
||||||
studyInstanceUIDs: this.props.studyInstanceUIDs,
|
|
||||||
PatientID: filter.PatientID,
|
|
||||||
earliestDate,
|
|
||||||
latestDate,
|
|
||||||
isLocked: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeTimepoints = timepointData => {
|
|
||||||
OHIF.log.info('storeTimepoints');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTimepoint = (timepointData, query) => {
|
|
||||||
OHIF.log.info('updateTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
removeTimepoint = timepointId => {
|
|
||||||
OHIF.log.info('removeTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
disassociateStudy = (timepointIds, StudyInstanceUID) => {
|
|
||||||
OHIF.log.info('disassociateStudy');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
onTimepointsUpdated = timepoints => {
|
|
||||||
if (this.props.onTimepointsUpdated) {
|
|
||||||
this.props.onTimepointsUpdated(timepoints);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMeasurementsUpdated = measurements => {
|
|
||||||
if (this.props.onMeasurementsUpdated) {
|
|
||||||
this.props.onMeasurementsUpdated(measurements);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { studies, isStudyLoaded } = this.props;
|
|
||||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
|
||||||
const currentTimepointId = 'TimepointId';
|
|
||||||
|
|
||||||
const timepointApi = new TimepointApi(currentTimepointId, {
|
|
||||||
onTimepointsUpdated: this.onTimepointsUpdated,
|
|
||||||
});
|
|
||||||
|
|
||||||
const measurementApi = new MeasurementApi(timepointApi, {
|
|
||||||
onMeasurementsUpdated: this.onMeasurementsUpdated,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentTimepointId = currentTimepointId;
|
|
||||||
this.timepointApi = timepointApi;
|
|
||||||
this.measurementApi = measurementApi;
|
|
||||||
|
|
||||||
if (studies) {
|
|
||||||
const PatientID = studies[0] && studies[0].PatientID;
|
|
||||||
|
|
||||||
timepointApi.retrieveTimepoints({ PatientID });
|
|
||||||
if (isStudyLoaded) {
|
|
||||||
this.measurementApi.retrieveMeasurements(PatientID, [
|
|
||||||
currentTimepointId,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { studies, isStudyLoaded } = this.props;
|
|
||||||
if (studies !== prevProps.studies) {
|
|
||||||
this.setState({
|
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
|
||||||
const PatientID = studies[0] && studies[0].PatientID;
|
|
||||||
const { currentTimepointId } = this;
|
|
||||||
|
|
||||||
this.timepointApi.retrieveTimepoints({ PatientID });
|
|
||||||
this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let VisiblePanelLeft, VisiblePanelRight;
|
|
||||||
const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL];
|
|
||||||
|
|
||||||
panelExtensions.forEach(panelExt => {
|
|
||||||
panelExt.module.components.forEach(comp => {
|
|
||||||
if (comp.id === this.state.selectedRightSidePanel) {
|
|
||||||
VisiblePanelRight = comp.component;
|
|
||||||
} else if (comp.id === this.state.selectedLeftSidePanel) {
|
|
||||||
VisiblePanelLeft = comp.component;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* HEADER */}
|
|
||||||
<WhiteLabelingContext.Consumer>
|
|
||||||
{whiteLabeling => (
|
|
||||||
<UserManagerContext.Consumer>
|
|
||||||
{userManager => (
|
|
||||||
<AppContext.Consumer>
|
|
||||||
{appContext => (
|
|
||||||
<ConnectedHeader
|
|
||||||
linkText={
|
|
||||||
appContext.appConfig.showStudyList
|
|
||||||
? 'Study List'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
linkPath={
|
|
||||||
appContext.appConfig.showStudyList ? '/' : undefined
|
|
||||||
}
|
|
||||||
userManager={userManager}
|
|
||||||
>
|
|
||||||
{whiteLabeling &&
|
|
||||||
whiteLabeling.createLogoComponentFn &&
|
|
||||||
whiteLabeling.createLogoComponentFn(React)}
|
|
||||||
</ConnectedHeader>
|
|
||||||
)}
|
|
||||||
</AppContext.Consumer>
|
|
||||||
)}
|
|
||||||
</UserManagerContext.Consumer>
|
|
||||||
)}
|
|
||||||
</WhiteLabelingContext.Consumer>
|
|
||||||
|
|
||||||
{/* TOOLBAR */}
|
|
||||||
<ToolbarRow
|
|
||||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
|
||||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
|
||||||
selectedLeftSidePanel={
|
|
||||||
this.state.isLeftSidePanelOpen
|
|
||||||
? this.state.selectedLeftSidePanel
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
selectedRightSidePanel={
|
|
||||||
this.state.isRightSidePanelOpen
|
|
||||||
? this.state.selectedRightSidePanel
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
handleSidePanelChange={(side, selectedPanel) => {
|
|
||||||
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
|
||||||
const openKey = `is${sideClicked}SidePanelOpen`;
|
|
||||||
const selectedKey = `selected${sideClicked}SidePanel`;
|
|
||||||
const updatedState = Object.assign({}, this.state);
|
|
||||||
|
|
||||||
const isOpen = updatedState[openKey];
|
|
||||||
const prevSelectedPanel = updatedState[selectedKey];
|
|
||||||
// RoundedButtonGroup returns `null` if selected button is clicked
|
|
||||||
const isSameSelectedPanel =
|
|
||||||
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
|
||||||
|
|
||||||
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
|
||||||
|
|
||||||
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
|
||||||
if (isClosedOrShouldClose) {
|
|
||||||
updatedState[openKey] = !updatedState[openKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(updatedState);
|
|
||||||
}}
|
|
||||||
studies={this.props.studies}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
|
||||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
|
||||||
|
|
||||||
{/* VIEWPORTS + SIDEPANELS */}
|
|
||||||
<div className="FlexboxLayout">
|
|
||||||
{/* LEFT */}
|
|
||||||
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
|
||||||
{VisiblePanelLeft ? (
|
|
||||||
<VisiblePanelLeft
|
|
||||||
viewports={this.props.viewports}
|
|
||||||
studies={this.props.studies}
|
|
||||||
activeIndex={this.props.activeViewportIndex}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ConnectedStudyBrowser
|
|
||||||
studies={this.state.thumbnails}
|
|
||||||
studyMetadata={this.props.studies}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SidePanel>
|
|
||||||
|
|
||||||
{/* MAIN */}
|
|
||||||
<div className={classNames('main-content')}>
|
|
||||||
<ConnectedViewerMain
|
|
||||||
studies={this.props.studies}
|
|
||||||
isStudyLoaded={this.props.isStudyLoaded}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RIGHT */}
|
|
||||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
|
||||||
{VisiblePanelRight && (
|
|
||||||
<VisiblePanelRight
|
|
||||||
isOpen={this.state.isRightSidePanelOpen}
|
|
||||||
viewports={this.props.viewports}
|
|
||||||
studies={this.props.studies}
|
|
||||||
activeIndex={this.props.activeViewportIndex}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SidePanel>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withDialog(Viewer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What types are these? Why do we have "mapping" dropped in here instead of in
|
|
||||||
* a mapping layer?
|
|
||||||
*
|
|
||||||
* TODO[react]:
|
|
||||||
* - Add sorting of display sets
|
|
||||||
* - Add showStackLoadingProgressBar option
|
|
||||||
*
|
|
||||||
* @param {Study[]} studies
|
|
||||||
* @param {DisplaySet[]} studies[].displaySets
|
|
||||||
*/
|
|
||||||
const _mapStudiesToThumbnails = function(studies) {
|
|
||||||
return studies.map(study => {
|
|
||||||
const { StudyInstanceUID } = study;
|
|
||||||
|
|
||||||
const thumbnails = study.displaySets.map(displaySet => {
|
|
||||||
const {
|
|
||||||
displaySetInstanceUID,
|
|
||||||
SeriesDescription,
|
|
||||||
SeriesNumber,
|
|
||||||
InstanceNumber,
|
|
||||||
numImageFrames,
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
let imageId;
|
|
||||||
let altImageText;
|
|
||||||
|
|
||||||
if (displaySet.Modality && displaySet.Modality === 'SEG') {
|
|
||||||
// TODO: We want to replace this with a thumbnail showing
|
|
||||||
// the segmentation map on the image, but this is easier
|
|
||||||
// and better than what we have right now.
|
|
||||||
altImageText = 'SEG';
|
|
||||||
} else if (displaySet.images && displaySet.images.length) {
|
|
||||||
const imageIndex = Math.floor(displaySet.images.length / 2);
|
|
||||||
|
|
||||||
imageId = displaySet.images[imageIndex].getImageId();
|
|
||||||
} else {
|
|
||||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageId,
|
|
||||||
altImageText,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
SeriesDescription,
|
|
||||||
SeriesNumber,
|
|
||||||
InstanceNumber,
|
|
||||||
numImageFrames,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
StudyInstanceUID,
|
|
||||||
thumbnails,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { metadata, utils } from '@ohif/core';
|
|
||||||
|
|
||||||
import ConnectedViewer from './ConnectedViewer.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
import Dropzone from 'react-dropzone';
|
|
||||||
import filesToStudies from '../lib/filesToStudies';
|
|
||||||
import './ViewerLocalFileData.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { OHIFStudyMetadata } = metadata;
|
|
||||||
const { studyMetadataManager } = utils;
|
|
||||||
|
|
||||||
const dropZoneLinkDialog = (onDrop, i18n, dir) => {
|
|
||||||
return (
|
|
||||||
<Dropzone onDrop={onDrop} noDrag>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<span {...getRootProps()} className="link-dialog">
|
|
||||||
{dir ? (
|
|
||||||
<span>
|
|
||||||
{i18n('Load folders')}
|
|
||||||
<input
|
|
||||||
{...getInputProps()}
|
|
||||||
webkitdirectory="true"
|
|
||||||
mozdirectory="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
{i18n('Load files')}
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const linksDialogMessage = (onDrop, i18n) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{i18n('Or click to ')}
|
|
||||||
{dropZoneLinkDialog(onDrop, i18n)}
|
|
||||||
{i18n(' or ')}
|
|
||||||
{dropZoneLinkDialog(onDrop, i18n, true)}
|
|
||||||
{i18n(' from dialog')}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
class ViewerLocalFileData extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
studies: null,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
updateStudies = studies => {
|
|
||||||
// Render the viewer when the data is ready
|
|
||||||
studyMetadataManager.purge();
|
|
||||||
|
|
||||||
// Map studies to new format, update metadata manager?
|
|
||||||
const updatedStudies = studies.map(study => {
|
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
|
||||||
study,
|
|
||||||
study.StudyInstanceUID
|
|
||||||
);
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
|
|
||||||
study.displaySets =
|
|
||||||
study.displaySets ||
|
|
||||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
|
||||||
|
|
||||||
studyMetadata.forEachDisplaySet(displayset => {
|
|
||||||
displayset.localFile = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
|
|
||||||
return study;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
studies: updatedStudies,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const onDrop = async acceptedFiles => {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
|
|
||||||
const studies = await filesToStudies(acceptedFiles);
|
|
||||||
const updatedStudies = this.updateStudies(studies);
|
|
||||||
|
|
||||||
if (!updatedStudies) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ studies: updatedStudies, loading: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.error) {
|
|
||||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropzone onDrop={onDrop} noClick>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
|
|
||||||
{this.state.studies ? (
|
|
||||||
<ConnectedViewer
|
|
||||||
studies={this.state.studies}
|
|
||||||
studyInstanceUIDs={
|
|
||||||
this.state.studies &&
|
|
||||||
this.state.studies.map(a => a.StudyInstanceUID)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={'drag-drop-instructions'}>
|
|
||||||
<div className={'drag-drop-contents'}>
|
|
||||||
{this.state.loading ? (
|
|
||||||
<h3>{this.props.t('Loading...')}</h3>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
{this.props.t(
|
|
||||||
'Drag and Drop DICOM files here to load them in the Viewer'
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<h4>{linksDialogMessage(onDrop, this.props.t)}</h4>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(ViewerLocalFileData);
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import './ViewerMain.css';
|
|
||||||
|
|
||||||
import { Component } from 'react';
|
|
||||||
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import memoize from 'lodash/memoize';
|
|
||||||
import _values from 'lodash/values';
|
|
||||||
|
|
||||||
var values = memoize(_values);
|
|
||||||
|
|
||||||
class ViewerMain extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
|
||||||
studies: PropTypes.array,
|
|
||||||
viewportSpecificData: PropTypes.object.isRequired,
|
|
||||||
layout: PropTypes.object.isRequired,
|
|
||||||
setViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
displaySets: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisplaySets(studies) {
|
|
||||||
const displaySets = [];
|
|
||||||
studies.forEach(study => {
|
|
||||||
study.displaySets.forEach(dSet => {
|
|
||||||
if (!dSet.plugin) {
|
|
||||||
dSet.plugin = 'cornerstone';
|
|
||||||
}
|
|
||||||
displaySets.push(dSet);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
findDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) {
|
|
||||||
const study = studies.find(study => {
|
|
||||||
return study.StudyInstanceUID === StudyInstanceUID;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!study) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return study.displaySets.find(displaySet => {
|
|
||||||
return displaySet.displaySetInstanceUID === displaySetInstanceUID;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// Add beforeUnload event handler to check for unsaved changes
|
|
||||||
//window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
|
|
||||||
|
|
||||||
// Get all the display sets for the viewer studies
|
|
||||||
if (this.props.studies) {
|
|
||||||
const displaySets = this.getDisplaySets(this.props.studies);
|
|
||||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const prevViewportAmount = prevProps.layout.viewports.length;
|
|
||||||
const viewportAmount = this.props.layout.viewports.length;
|
|
||||||
const isVtk = this.props.layout.viewports.some(vp => !!vp.vtk);
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.studies !== prevProps.studies ||
|
|
||||||
(viewportAmount !== prevViewportAmount && !isVtk)
|
|
||||||
) {
|
|
||||||
const displaySets = this.getDisplaySets(this.props.studies);
|
|
||||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillEmptyViewportPanes = () => {
|
|
||||||
// TODO: Here is the entry point for filling viewports on load.
|
|
||||||
const dirtyViewportPanes = [];
|
|
||||||
const { layout, viewportSpecificData } = this.props;
|
|
||||||
const { displaySets } = this.state;
|
|
||||||
|
|
||||||
if (!displaySets || !displaySets.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < layout.viewports.length; i++) {
|
|
||||||
const viewportPane = viewportSpecificData[i];
|
|
||||||
const isNonEmptyViewport =
|
|
||||||
viewportPane &&
|
|
||||||
viewportPane.StudyInstanceUID &&
|
|
||||||
viewportPane.displaySetInstanceUID;
|
|
||||||
|
|
||||||
if (isNonEmptyViewport) {
|
|
||||||
dirtyViewportPanes.push({
|
|
||||||
StudyInstanceUID: viewportPane.StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: viewportPane.displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundDisplaySet =
|
|
||||||
displaySets.find(
|
|
||||||
ds =>
|
|
||||||
!dirtyViewportPanes.some(
|
|
||||||
v => v.displaySetInstanceUID === ds.displaySetInstanceUID
|
|
||||||
)
|
|
||||||
) || displaySets[displaySets.length - 1];
|
|
||||||
|
|
||||||
dirtyViewportPanes.push(foundDisplaySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
dirtyViewportPanes.forEach((vp, i) => {
|
|
||||||
if (vp && vp.StudyInstanceUID) {
|
|
||||||
this.setViewportData({
|
|
||||||
viewportIndex: i,
|
|
||||||
StudyInstanceUID: vp.StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: vp.displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setViewportData = ({
|
|
||||||
viewportIndex,
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
}) => {
|
|
||||||
const displaySet = this.findDisplaySet(
|
|
||||||
this.props.studies,
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
this.props.setViewportSpecificData(viewportIndex, displaySet);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { viewportSpecificData } = this.props;
|
|
||||||
const viewportData = values(viewportSpecificData);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ViewerMain">
|
|
||||||
{this.state.displaySets.length && (
|
|
||||||
<ConnectedViewportGrid
|
|
||||||
isStudyLoaded={this.props.isStudyLoaded}
|
|
||||||
studies={this.props.studies}
|
|
||||||
viewportData={viewportData}
|
|
||||||
setViewportData={this.setViewportData}
|
|
||||||
>
|
|
||||||
{/* Children to add to each viewport that support children */}
|
|
||||||
</ConnectedViewportGrid>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Clear the entire viewport specific data
|
|
||||||
const { viewportSpecificData } = this.props;
|
|
||||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
|
||||||
this.props.clearViewportSpecificData(viewportIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: These don't have to be viewer specific?
|
|
||||||
// Could qualify for other routes?
|
|
||||||
// hotkeys.destroy();
|
|
||||||
|
|
||||||
// Remove beforeUnload event handler...
|
|
||||||
//window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
|
|
||||||
// Destroy the synchronizer used to update reference lines
|
|
||||||
//OHIF.viewer.updateImageSynchronizer.destroy();
|
|
||||||
// TODO: Instruct all plugins to clean up themselves
|
|
||||||
//
|
|
||||||
// Clear references to all stacks in the StackManager
|
|
||||||
//StackManager.clearStacks();
|
|
||||||
// @TypeSafeStudies
|
|
||||||
// Clears OHIF.viewer.Studies collection
|
|
||||||
//OHIF.viewer.Studies.removeAll();
|
|
||||||
// @TypeSafeStudies
|
|
||||||
// Clears OHIF.viewer.StudyMetadataList collection
|
|
||||||
//OHIF.viewer.StudyMetadataList.removeAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ViewerMain;
|
|
||||||
@ -1,375 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
|
||||||
import { metadata, studies, utils, log } from '@ohif/core';
|
|
||||||
import usePrevious from '../customHooks/usePrevious';
|
|
||||||
|
|
||||||
import ConnectedViewer from './ConnectedViewer.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
import { useSnackbarContext } from '@ohif/ui';
|
|
||||||
|
|
||||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
|
||||||
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
|
||||||
const { studyMetadataManager, makeCancelable } = utils;
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import AppContext from '../context/AppContext';
|
|
||||||
|
|
||||||
const _promoteToFront = (list, value, searchMethod) => {
|
|
||||||
let response = [...list];
|
|
||||||
let promoted = false;
|
|
||||||
const index = response.findIndex(searchMethod.bind(undefined, value));
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
const first = response.splice(index, 1);
|
|
||||||
response = [...first, ...response];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
promoted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
promoted,
|
|
||||||
data: response,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Promote series to front if find found equivalent on filters object
|
|
||||||
* @param {Object} study - study reference to promote series against
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
|
||||||
*/
|
|
||||||
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
|
|
||||||
let promoted = false;
|
|
||||||
// Promote only if no filter should be applied
|
|
||||||
if (!isFilterStrategy) {
|
|
||||||
_sortStudyDisplaySet(study, studyMetadata);
|
|
||||||
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promoted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
|
|
||||||
let promoted = false;
|
|
||||||
const queryParamsLength = Object.keys(filters).length;
|
|
||||||
const shouldPromoteToFront = queryParamsLength > 0;
|
|
||||||
|
|
||||||
if (shouldPromoteToFront) {
|
|
||||||
const { seriesInstanceUID } = filters;
|
|
||||||
|
|
||||||
const _seriesLookup = (valueToCompare, displaySet) => {
|
|
||||||
return displaySet.SeriesInstanceUID === valueToCompare;
|
|
||||||
};
|
|
||||||
const promotedResponse = _promoteToFront(
|
|
||||||
studyMetadata.getDisplaySets(),
|
|
||||||
seriesInstanceUID,
|
|
||||||
_seriesLookup
|
|
||||||
);
|
|
||||||
|
|
||||||
study.displaySets = promotedResponse.data;
|
|
||||||
promoted = promotedResponse.promoted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return promoted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to identify if query param (from url) was applied to given list
|
|
||||||
* @param {Object} study - study reference to promote series against
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
|
||||||
*/
|
|
||||||
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
|
|
||||||
const { seriesInstanceUID } = filters;
|
|
||||||
let applied = true;
|
|
||||||
// skip in case no filter or no toast manager
|
|
||||||
if (!seriesInstanceUID) {
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { series = [], displaySets = [] } = study;
|
|
||||||
const firstSeries = isFilterStrategy ? series[0] : displaySets[0];
|
|
||||||
|
|
||||||
if (!firstSeries || firstSeries.SeriesInstanceUID !== seriesInstanceUID) {
|
|
||||||
applied = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return applied;
|
|
||||||
};
|
|
||||||
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
|
||||||
if (queryParamApplied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { show: showUserMessage = () => {} } = dialog;
|
|
||||||
showUserMessage({
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const _addSeriesToStudy = (studyMetadata, series) => {
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
const study = studyMetadata.getData();
|
|
||||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
|
||||||
const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID);
|
|
||||||
if (existingSeries) {
|
|
||||||
studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata);
|
|
||||||
} else {
|
|
||||||
studyMetadata.addSeries(seriesMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
|
||||||
sopClassHandlerModules,
|
|
||||||
seriesMetadata
|
|
||||||
);
|
|
||||||
study.displaySets = studyMetadata.getDisplaySets();
|
|
||||||
_updateStudyMetadataManager(study, studyMetadata);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _updateStudyMetadataManager = (study, studyMetadata) => {
|
|
||||||
const { StudyInstanceUID } = study;
|
|
||||||
|
|
||||||
if (!studyMetadataManager.get(StudyInstanceUID)) {
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _updateStudyDisplaySets = (study, studyMetadata) => {
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
|
|
||||||
if (!study.displaySets) {
|
|
||||||
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
|
||||||
studyMetadata.sortDisplaySets(study.displaySets);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ViewerRetrieveStudyData({
|
|
||||||
server,
|
|
||||||
studyInstanceUIDs,
|
|
||||||
seriesInstanceUIDs,
|
|
||||||
clearViewportSpecificData,
|
|
||||||
}) {
|
|
||||||
// hooks
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const [studies, setStudies] = useState([]);
|
|
||||||
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
|
||||||
const snackbarContext = useSnackbarContext();
|
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
|
||||||
const {
|
|
||||||
filterQueryParam: isFilterStrategy = false,
|
|
||||||
maxConcurrentMetadataRequests,
|
|
||||||
} = appConfig;
|
|
||||||
|
|
||||||
let cancelableSeriesPromises;
|
|
||||||
let cancelableStudiesPromises;
|
|
||||||
/**
|
|
||||||
* Callback method when study is totally loaded
|
|
||||||
* @param {object} study study loaded
|
|
||||||
* @param {object} studyMetadata studyMetadata for given study
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
*/
|
|
||||||
const studyDidLoad = (study, studyMetadata, filters) => {
|
|
||||||
// User message
|
|
||||||
const promoted = _promoteList(
|
|
||||||
study,
|
|
||||||
studyMetadata,
|
|
||||||
filters,
|
|
||||||
isFilterStrategy
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear viewport to allow new promoted one to be displayed
|
|
||||||
if (promoted) {
|
|
||||||
clearViewportSpecificData(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isQueryParamApplied = _isQueryParamApplied(
|
|
||||||
study,
|
|
||||||
filters,
|
|
||||||
isFilterStrategy
|
|
||||||
);
|
|
||||||
// Show message in case not promoted neither filtered but should to
|
|
||||||
_showUserMessage(
|
|
||||||
isQueryParamApplied,
|
|
||||||
'Query parameters were not applied. Using original series list for given study.',
|
|
||||||
snackbarContext
|
|
||||||
);
|
|
||||||
|
|
||||||
setStudies([...studies, study]);
|
|
||||||
setIsStudyLoaded(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
|
|
||||||
* @param {Array} studiesData Array of studies retrieved from server
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
*/
|
|
||||||
const processStudies = (studiesData, filters) => {
|
|
||||||
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
|
||||||
// Map studies to new format, update metadata manager?
|
|
||||||
const studies = studiesData.map(study => {
|
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
|
||||||
study,
|
|
||||||
study.StudyInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
_updateStudyDisplaySets(study, studyMetadata);
|
|
||||||
_updateStudyMetadataManager(study, studyMetadata);
|
|
||||||
|
|
||||||
// Attempt to load remaning series if any
|
|
||||||
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
|
||||||
loadRemainingSeries(studyMetadata)
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
if (result && !result.isCanceled) {
|
|
||||||
studyDidLoad(study, studyMetadata, filters);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error && !error.isCanceled) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return study;
|
|
||||||
});
|
|
||||||
|
|
||||||
setStudies(studies);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceRerender = () => setStudies(studies => [...studies]);
|
|
||||||
|
|
||||||
const loadRemainingSeries = async studyMetadata => {
|
|
||||||
const { seriesLoader } = studyMetadata.getData();
|
|
||||||
if (!seriesLoader) return;
|
|
||||||
|
|
||||||
const loadNextSeries = async () => {
|
|
||||||
if (!seriesLoader.hasNext()) return;
|
|
||||||
const series = await seriesLoader.next();
|
|
||||||
_addSeriesToStudy(studyMetadata, series);
|
|
||||||
forceRerender();
|
|
||||||
return loadNextSeries();
|
|
||||||
};
|
|
||||||
|
|
||||||
const concurrentRequestsAllowed =
|
|
||||||
maxConcurrentMetadataRequests || studyMetadata.getSeriesCount();
|
|
||||||
const promises = Array(concurrentRequestsAllowed)
|
|
||||||
.fill(null)
|
|
||||||
.map(loadNextSeries);
|
|
||||||
|
|
||||||
return await Promise.all(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadStudies = async () => {
|
|
||||||
try {
|
|
||||||
const filters = {};
|
|
||||||
// Use the first, discard others
|
|
||||||
const seriesInstanceUID = seriesInstanceUIDs && seriesInstanceUIDs[0];
|
|
||||||
|
|
||||||
const retrieveParams = [server, studyInstanceUIDs];
|
|
||||||
|
|
||||||
if (seriesInstanceUID) {
|
|
||||||
filters.seriesInstanceUID = seriesInstanceUID;
|
|
||||||
// Query param filtering controlled by appConfig property
|
|
||||||
if (isFilterStrategy) {
|
|
||||||
retrieveParams.push(filters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelableStudiesPromises[studyInstanceUIDs] = makeCancelable(
|
|
||||||
retrieveStudiesMetadata(...retrieveParams)
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
if (result && !result.isCanceled) {
|
|
||||||
processStudies(result, filters);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error && !error.isCanceled) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const purgeCancellablePromises = () => {
|
|
||||||
for (let studyInstanceUIDs in cancelableStudiesPromises) {
|
|
||||||
if ('cancel' in cancelableStudiesPromises[studyInstanceUIDs]) {
|
|
||||||
cancelableStudiesPromises[studyInstanceUIDs].cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let studyInstanceUIDs in cancelableSeriesPromises) {
|
|
||||||
if ('cancel' in cancelableSeriesPromises[studyInstanceUIDs]) {
|
|
||||||
cancelableSeriesPromises[studyInstanceUIDs].cancel();
|
|
||||||
deleteStudyMetadataPromise(studyInstanceUIDs);
|
|
||||||
studyMetadataManager.remove(studyInstanceUIDs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStudyInstanceUIDs = usePrevious(studyInstanceUIDs);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hasStudyInstanceUIDsChanged = !(
|
|
||||||
prevStudyInstanceUIDs &&
|
|
||||||
prevStudyInstanceUIDs.every(e => studyInstanceUIDs.includes(e))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasStudyInstanceUIDsChanged) {
|
|
||||||
studyMetadataManager.purge();
|
|
||||||
purgeCancellablePromises();
|
|
||||||
}
|
|
||||||
}, [studyInstanceUIDs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cancelableSeriesPromises = {};
|
|
||||||
cancelableStudiesPromises = {};
|
|
||||||
loadStudies();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
purgeCancellablePromises();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div>Error: {JSON.stringify(error)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConnectedViewer
|
|
||||||
studies={studies}
|
|
||||||
isStudyLoaded={isStudyLoaded}
|
|
||||||
studyInstanceUIDs={studyInstanceUIDs}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewerRetrieveStudyData.propTypes = {
|
|
||||||
studyInstanceUIDs: PropTypes.array.isRequired,
|
|
||||||
seriesInstanceUIDs: PropTypes.array,
|
|
||||||
server: PropTypes.object,
|
|
||||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewerRetrieveStudyData;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Finds displaySet by UID across all displaySets inside studyMetadata
|
|
||||||
* @param {Array} studyMetadata
|
|
||||||
* @param {string} displaySetInstanceUID
|
|
||||||
*/
|
|
||||||
export default function findDisplaySetByUID(
|
|
||||||
studyMetadata,
|
|
||||||
displaySetInstanceUID
|
|
||||||
) {
|
|
||||||
if (!Array.isArray(studyMetadata)) return null;
|
|
||||||
|
|
||||||
const allDisplaySets = studyMetadata.reduce((all, current) => {
|
|
||||||
let currentDisplaySet = [];
|
|
||||||
if (current && Array.isArray(current.displaySets)) {
|
|
||||||
currentDisplaySet = current.displaySets;
|
|
||||||
}
|
|
||||||
return all.concat(currentDisplaySet);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const bySetInstanceUID = ds =>
|
|
||||||
ds.displaySetInstanceUID === displaySetInstanceUID;
|
|
||||||
|
|
||||||
const displaySet = allDisplaySets.find(bySetInstanceUID);
|
|
||||||
return displaySet || null;
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
|
||||||
|
|
||||||
describe('findDisplaySetByUID', () => {
|
|
||||||
test('returns null when studyMetadata isnt an array', () => {
|
|
||||||
const result = findDisplaySetByUID(undefined, 'hello');
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns null when no match found', () => {
|
|
||||||
const result = findDisplaySetByUID([], 'no-match');
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it handles missing displaySet arrays', () => {
|
|
||||||
const expected = '9388-2291-a8fe';
|
|
||||||
const studyMetadata = [
|
|
||||||
{ displaySets: null },
|
|
||||||
{
|
|
||||||
displaySets: [{ displaySetInstanceUID: expected }],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
7,
|
|
||||||
];
|
|
||||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
|
||||||
expect(result.displaySetInstanceUID).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct displaySet by UID', () => {
|
|
||||||
const expected = '1234-5678';
|
|
||||||
const studyMetadata = [
|
|
||||||
{ displaySets: [{ displaySetInstanceUID: '0011-2239' }] },
|
|
||||||
{
|
|
||||||
displaySets: [
|
|
||||||
{ displaySetInstanceUID: '0392-2211' },
|
|
||||||
{ displaySetInstanceUID: expected },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ displaySets: [{ displaySetInstanceUID: '3384-9933' }] },
|
|
||||||
];
|
|
||||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
|
||||||
expect(result.displaySetInstanceUID).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user