From e943f601bc74b9273381a7c0b6f09154514a9fff Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 19 Apr 2022 13:44:19 -0400 Subject: [PATCH] feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock --- .../Viewport/OHIFCornerstone3DViewport.css | 19 ++ .../OHIFCornerstone3DViewport.tsx | 189 ++++++++++---- .../cornerstone-3d/src/commandsModule.js | 211 +++++++++++++++- extensions/cornerstone-3d/src/index.tsx | 9 +- extensions/cornerstone-3d/src/init.js | 25 +- .../src/initMeasurementService.js | 94 ++++--- .../cornerstone-3d/src/initWADOImageLoader.js | 14 +- .../ToolGroupService/ToolGroupService.ts | 37 ++- ...ice.ts => Cornerstone3DViewportService.ts} | 107 ++++---- .../src/services/ViewportService/Viewport.ts | 15 +- .../src/services/ViewportService/index.js | 10 - .../Bidirectional.js | 22 +- .../EllipticalROI.js | 31 ++- .../measurementServiceMappings/Length.js | 21 +- .../RectangleROI.js | 36 ++- .../measurementServiceMappingsFactory.js | 10 +- .../utils/getSOPInstanceAttributes.js | 8 +- .../default/src/DicomWebDataSource/index.js | 1 + .../wado/retrieveMetadataLoaderAsync.js | 1 - extensions/default/src/ViewerLayout/index.tsx | 2 +- .../default/src/getHangingProtocolModule.js | 230 +++++++++++++++++- modes/basic-viewer-cs3d/src/index.js | 25 +- modes/basic-viewer-cs3d/src/toolbarButtons.js | 20 +- platform/core/src/defaults/hotkeyBindings.js | 82 ++++++- .../DicomMetadataStore/DicomMetadataStore.js | 1 + .../DicomMetadataStore/createStudyMetadata.js | 7 +- .../HangingProtocolService/HPMatcher.js | 9 +- .../HangingProtocolService.js | 63 +++-- .../MeasurementService/MeasurementService.js | 25 +- .../ImageScrollbar/ImageScrollbar.css | 106 ++++++++ .../ImageScrollbar/ImageScrollbar.tsx | 67 +++++ .../ui/src/components/ImageScrollbar/index.js | 2 + .../components/ViewportGrid/ViewportGrid.tsx | 3 +- .../components/ViewportPane/ViewportPane.tsx | 5 +- platform/ui/src/components/index.js | 2 + .../contextProviders/ViewportGridProvider.tsx | 19 +- platform/ui/src/index.js | 1 + platform/viewer/public/config/default.js | 1 + .../viewer/src/components/ViewportGrid.tsx | 114 +++------ yarn.lock | 182 +++++++------- 40 files changed, 1357 insertions(+), 469 deletions(-) create mode 100644 extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.css rename extensions/cornerstone-3d/src/{ => Viewport}/OHIFCornerstone3DViewport.tsx (57%) rename extensions/cornerstone-3d/src/services/ViewportService/{ViewportService.ts => Cornerstone3DViewportService.ts} (73%) delete mode 100644 extensions/cornerstone-3d/src/services/ViewportService/index.js create mode 100644 platform/ui/src/components/ImageScrollbar/ImageScrollbar.css create mode 100644 platform/ui/src/components/ImageScrollbar/ImageScrollbar.tsx create mode 100644 platform/ui/src/components/ImageScrollbar/index.js diff --git a/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.css b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.css new file mode 100644 index 00000000000..d77867711d7 --- /dev/null +++ b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.css @@ -0,0 +1,19 @@ +.viewport-wrapper { + width: 100%; + height: 100%; /* MUST have `height` to prevent resize infinite loop */ + position: relative; +} + +.viewport-element { + width: 100%; + height: 100%; + position: relative; + background-color: black; + + /* Prevent the blue outline in Chrome when a viewport is selected */ + outline: 0 !important; + + /* Prevents the entire page from getting larger + when the magnify tool is near the sides/corners of the page */ + overflow: hidden; +} diff --git a/extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx similarity index 57% rename from extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx rename to extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx index 6f7c1ef8826..a9ac1f07dc7 100644 --- a/extensions/cornerstone-3d/src/OHIFCornerstone3DViewport.tsx +++ b/extensions/cornerstone-3d/src/Viewport/OHIFCornerstone3DViewport.tsx @@ -1,7 +1,17 @@ -import React, { useEffect, useRef, useCallback } from 'react'; -import { utilities } from '@cornerstonejs/tools'; +import React, { useEffect, useRef, useCallback, useState } from 'react'; import ReactResizeDetector from 'react-resize-detector'; -import { useViewportGrid } from '@ohif/ui'; +import { useViewportGrid, ImageScrollbar } from '@ohif/ui'; +import OHIF from '@ohif/core'; +import { utilities } from '@cornerstonejs/tools'; +import { Enums } from '@cornerstonejs/core'; + +import Cornerstone3DViewportService from '../services/ViewportService/Cornerstone3DViewportService'; + +import './OHIFCornerstone3DViewport.css'; + +const { StackManager } = OHIF.utils; + +const STACK = 'stack'; function areEqual(prevProps, nextProps) { if (nextProps.needsRerendering) { @@ -18,14 +28,17 @@ function areEqual(prevProps, nextProps) { const nextDisplaySets = nextProps.displaySets[0]; if (prevDisplaySets && nextDisplaySets) { - return ( + const areSameDisplaySetInstanceUIDs = prevDisplaySets.displaySetInstanceUID === - nextDisplaySets.displaySetInstanceUID && - prevDisplaySets.images.length === nextDisplaySets.images.length && - prevDisplaySets.images.every( - (prevImage, index) => - prevImage.imageId === nextDisplaySets.images[index].imageId - ) + nextDisplaySets.displaySetInstanceUID; + const areSameImageLength = + prevDisplaySets.images.length === nextDisplaySets.images.length; + const areSameImageIds = prevDisplaySets.images.every( + (prevImage, index) => + prevImage.imageId === nextDisplaySets.images[index].imageId + ); + return ( + areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds ); } return false; @@ -43,56 +56,83 @@ const OHIFCornerstoneViewport = React.memo(props => { servicesManager, } = props; + const [viewportData, setViewportData] = useState(null); + const [scrollbarIndex, setScrollbarIndex] = useState(0); + const [scrollbarHeight, setScrollbarHeight] = useState('100px'); const [_, viewportGridService] = useViewportGrid(); const elementRef = useRef(); - const { - ViewportService, - MeasurementService, - DisplaySetService, - } = servicesManager.services; + const { MeasurementService, DisplaySetService } = servicesManager.services; + + // useCallback for scroll bar height calculation + const setImageScrollBarHeight = useCallback(() => { + const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`; + setScrollbarHeight(scrollbarHeight); + }, [elementRef]); // useCallback for onResize - const onResize = useCallback( - props => { - if (elementRef.current) { - const element = elementRef.current; - ViewportService.resize(); - } - }, - [elementRef] - ); + const onResize = useCallback(() => { + if (elementRef.current) { + Cornerstone3DViewportService.resize(); + setImageScrollBarHeight(); + } + }, [elementRef]); // disable the element upon unmounting useEffect(() => { - // setElementRef(targetRef.current); - ViewportService.enableElement(viewportIndex, elementRef.current); + Cornerstone3DViewportService.enableElement( + viewportIndex, + elementRef.current + ); + setImageScrollBarHeight(); return () => { - ViewportService.disableElement(viewportIndex); + Cornerstone3DViewportService.disableElement(viewportIndex); }; }, []); - // Todo: Use stackManager to handle the stack creation and imageId get, problem - // would be what to do with the volumes which needs different schema for loading: streaming-wadors - // Stack manager shouldn't care about the type of volume. Maybe add a postImageId creation callback? - // Maybe we should need a volumeManager later on. useEffect(() => { - ViewportService.setViewportDisplaySets( - viewportIndex, + const viewportData = _getViewportData( + dataSource, displaySets, + viewportOptions.viewportType + ); + + Cornerstone3DViewportService.setViewportDisplaySets( + viewportIndex, + viewportData, viewportOptions, - displaySetOptions, - dataSource + displaySetOptions ); + + setViewportData(viewportData); }, [ viewportIndex, viewportOptions, displaySetOptions, displaySets, dataSource, - ViewportService, ]); + useEffect(() => { + const element = elementRef.current; + + const updateIndex = event => { + const { imageId } = event.detail; + // find the index of imageId in the imageIds + const index = viewportData.stack?.imageIds.indexOf(imageId); + + if (index !== -1) { + setScrollbarIndex(index); + } + }; + + element.addEventListener(Enums.Events.STACK_NEW_IMAGE, updateIndex); + + return () => { + element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, updateIndex); + }; + }, [elementRef, viewportData]); + /** * There are two scenarios for jump to click * 1. Current viewports contain the displaySet that the annotation was drawn on @@ -125,17 +165,37 @@ const OHIFCornerstoneViewport = React.memo(props => { return () => { unsubscribeFromJumpToMeasurementEvents(); }; - }, [ - displaySets, - elementRef, - viewportIndex, - viewportGridService, - MeasurementService, - DisplaySetService, - ]); + }, [displaySets, elementRef, viewportIndex]); + + const onImageScrollbarChange = useCallback( + (imageIndex, viewportIndex) => { + const viewportInfo = Cornerstone3DViewportService.getViewportInfoByIndex( + viewportIndex + ); + + const viewportId = viewportInfo.getViewportId(); + const viewport = Cornerstone3DViewportService.getCornerstone3DViewport( + viewportId + ); + + // if getCurrentImageId is not a method on viewport + if (!viewport.getCurrentImageId) { + throw new Error('cannot use scrollbar for non-stack viewports'); + } + + // Later scrollThroughStack should return two values the current index + // and the total number of indices (volume it is different) + viewport.setImageIdIndex(imageIndex).then(() => { + // Update scrollbar index + const currentIndex = viewport.getCurrentImageIdIndex(); + setScrollbarIndex(currentIndex); + }); + }, + [viewportIndex, viewportData] + ); return ( - +
{ onMouseDown={e => e.preventDefault()} ref={elementRef} >
-
+ onImageScrollbarChange(evt, viewportIndex)} + max={viewportData ? viewportData.stack?.imageIds?.length - 1 : 0} + height={scrollbarHeight} + value={scrollbarIndex} + /> + ); }, areEqual); +function _getCornerstoneStack(displaySet, dataSource) { + // Get stack from Stack Manager + const storedStack = StackManager.findOrCreateStack(displaySet, dataSource); + + // Clone the stack here so we don't mutate it + const stack = Object.assign({}, storedStack); + + return stack; +} + +function _getViewportData(dataSource, displaySets, viewportType) { + viewportType = viewportType || STACK; + if (viewportType !== STACK) { + throw new Error('Only STACK viewport type is supported now'); + } + + // For Stack Viewport we don't have fusion currently + const displaySet = displaySets[0]; + + const stack = _getCornerstoneStack(displaySet, dataSource); + + const viewportData = { + StudyInstanceUID: displaySet.StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + stack, + }; + + return viewportData; +} + function _subscribeToJumpToMeasurementEvents( MeasurementService, DisplaySetService, @@ -261,6 +357,7 @@ function _jumpToMeasurement( }; utilities.jumpToSlice(targetElement, metadata); + annotations.selection.setAnnotationSelected(); // Jump to measurement consumed, remove. MeasurementService.removeJumpToMeasurement(viewportIndex); } diff --git a/extensions/cornerstone-3d/src/commandsModule.js b/extensions/cornerstone-3d/src/commandsModule.js index fba87b92c01..a31d37137dd 100644 --- a/extensions/cornerstone-3d/src/commandsModule.js +++ b/extensions/cornerstone-3d/src/commandsModule.js @@ -1,14 +1,12 @@ import * as cornerstone3D from '@cornerstonejs/core'; +import * as cornerstone3DTools from '@cornerstonejs/tools'; +import Cornerstone3DViewportService from './services/ViewportService/Cornerstone3DViewportService'; import { Enums } from '@cornerstonejs/tools'; import { getEnabledElement } from './state'; const commandsModule = ({ servicesManager }) => { - const { - ViewportGridService, - ToolGroupService, - ViewportService, - } = servicesManager.services; + const { ViewportGridService, ToolGroupService } = servicesManager.services; function _getActiveViewportEnabledElement() { const { activeViewportIndex } = ViewportGridService.getState(); @@ -37,7 +35,7 @@ const commandsModule = ({ servicesManager }) => { } // get actor from the viewport - const renderingEngine = ViewportService.getRenderingEngine(); + const renderingEngine = Cornerstone3DViewportService.getRenderingEngine(); const viewport = renderingEngine.getViewport(viewportId); const lower = windowCenterNum - windowWidthNum / 2.0; @@ -58,15 +56,29 @@ const commandsModule = ({ servicesManager }) => { let toolGroupIdToUse = toolGroupId; if (!toolGroupIdToUse) { - const toolGroupIds = ToolGroupService.getToolGroupIds(); + // Use the active viewport's tool group if no tool group id is provided + const enabledElement = _getActiveViewportEnabledElement(); - if (toolGroupIds.length !== 1) { - throw new Error( - 'setToolActive requires a toolGroupId if there are multiple tool groups' + if (!enabledElement) { + return; + } + + const { renderingEngineId, viewportId } = enabledElement; + const toolGroup = cornerstone3DTools.ToolGroupManager.getToolGroupForViewport( + viewportId, + renderingEngineId + ); + + if (!toolGroup) { + console.warn( + 'No tool group found for viewportId:', + viewportId, + 'and renderingEngineId:', + renderingEngineId ); } - toolGroupIdToUse = toolGroupIds[0]; + toolGroupIdToUse = toolGroup.id; } const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse); @@ -114,6 +126,128 @@ const commandsModule = ({ servicesManager }) => { return; } }, + rotateViewport: ({ rotation }) => { + const enabledElement = _getActiveViewportEnabledElement(); + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + const { rotation: currentRotation } = viewport.getProperties(); + + viewport.setProperties({ rotation: currentRotation + rotation }); + viewport.render(); + } + }, + flipViewportHorizontal: () => { + const enabledElement = _getActiveViewportEnabledElement(); + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + const { flipHorizontal } = viewport.getProperties(); + viewport.setProperties({ flipHorizontal: !flipHorizontal }); + viewport.render(); + } + }, + flipViewportVertical: () => { + const enabledElement = _getActiveViewportEnabledElement(); + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + const { flipVertical } = viewport.getProperties(); + viewport.setProperties({ flipVertical: !flipVertical }); + viewport.render(); + } + }, + invertViewport: ({ element }) => { + let enabledElement; + + if (element === undefined) { + enabledElement = _getActiveViewportEnabledElement(); + } else { + enabledElement = element; + } + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + const { invert } = viewport.getProperties(); + viewport.setProperties({ invert: !invert }); + viewport.render(); + } + }, + resetViewport: () => { + const enabledElement = _getActiveViewportEnabledElement(); + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + viewport.resetProperties(); + viewport.resetCamera(); + viewport.render(); + } + }, + scaleViewport: ({ direction }) => { + const enabledElement = _getActiveViewportEnabledElement(); + const scaleFactor = direction > 0 ? 0.9 : 1.1; + + if (!enabledElement) { + return; + } + const { viewport } = enabledElement; + + if (viewport instanceof cornerstone3D.StackViewport) { + if (direction) { + const { parallelScale } = viewport.getCamera(); + viewport.setCamera({ parallelScale: parallelScale * scaleFactor }); + viewport.render(); + } else { + viewport.resetCamera(); + viewport.render(); + } + } + }, + scroll: ({ direction }) => { + const enabledElement = _getActiveViewportEnabledElement(); + + if (!enabledElement) { + return; + } + + const { viewport } = enabledElement; + + let options = {}; + if (viewport instanceof cornerstone3D.StackViewport) { + options = { direction }; + } else { + throw new Error('scroll: volume viewport is not supported yet'); + } + + cornerstone3DTools.utilities.stackScrollTool.scrollThroughStack( + viewport, + options + ); + }, }; const definitions = { @@ -127,6 +261,61 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + rotateViewportCW: { + commandFn: actions.rotateViewport, + storeContexts: [], + options: { rotation: 90 }, + }, + rotateViewportCCW: { + commandFn: actions.rotateViewport, + storeContexts: [], + options: { rotation: -90 }, + }, + flipViewportHorizontal: { + commandFn: actions.flipViewportHorizontal, + storeContexts: [], + options: {}, + }, + flipViewportVertical: { + commandFn: actions.flipViewportVertical, + storeContexts: [], + options: {}, + }, + invertViewport: { + commandFn: actions.invertViewport, + storeContexts: [], + options: {}, + }, + resetViewport: { + commandFn: actions.resetViewport, + storeContexts: [], + options: {}, + }, + scaleUpViewport: { + commandFn: actions.scaleViewport, + storeContexts: [], + options: { direction: 1 }, + }, + scaleDownViewport: { + commandFn: actions.scaleViewport, + storeContexts: [], + options: { direction: -1 }, + }, + fitViewportToWindow: { + commandFn: actions.scaleViewport, + storeContexts: [], + options: { direction: 0 }, + }, + nextImage: { + commandFn: actions.scroll, + storeContexts: [], + options: { direction: 1 }, + }, + previousImage: { + commandFn: actions.scroll, + storeContexts: [], + options: { direction: -1 }, + }, }; return { diff --git a/extensions/cornerstone-3d/src/index.tsx b/extensions/cornerstone-3d/src/index.tsx index 7ff12a11f5c..422c7c011c2 100644 --- a/extensions/cornerstone-3d/src/index.tsx +++ b/extensions/cornerstone-3d/src/index.tsx @@ -6,14 +6,15 @@ import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import init from './init.js'; import commandsModule from './commandsModule'; import ToolGroupService from './services/ToolGroupService'; -import ViewportService from './services/ViewportService'; import { toolNames } from './initCornerstoneTools'; import { getEnabledElement } from './state'; import { id } from './id'; const Component = React.lazy(() => { - return import(/* webpackPrefetch: true */ './OHIFCornerstone3DViewport'); + return import( + /* webpackPrefetch: true */ './Viewport/OHIFCornerstone3DViewport' + ); }); const OHIFCornerstoneViewport = props => { @@ -43,10 +44,10 @@ const cornerstone3DExtension = { servicesManager, commandsManager, configuration = {}, + appConfig, }) { servicesManager.registerService(ToolGroupService(servicesManager)); - servicesManager.registerService(ViewportService(servicesManager)); - await init({ servicesManager, commandsManager, configuration }); + await init({ servicesManager, commandsManager, configuration, appConfig }); }, getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneViewport = props => { diff --git a/extensions/cornerstone-3d/src/init.js b/extensions/cornerstone-3d/src/init.js index 2c71d50b45c..03d72aaf301 100644 --- a/extensions/cornerstone-3d/src/init.js +++ b/extensions/cornerstone-3d/src/init.js @@ -11,6 +11,7 @@ import { import { Enums, utilities } from '@cornerstonejs/tools'; import initWADOImageLoader from './initWADOImageLoader'; +import Cornerstone3DViewportService from './services/ViewportService/Cornerstone3DViewportService'; import initCornerstoneTools from './initCornerstoneTools'; import { setEnabledElement } from './state'; @@ -31,6 +32,7 @@ export default async function init({ servicesManager, commandsManager, configuration, + appConfig, }) { await cs3DInit(); @@ -46,7 +48,6 @@ export default async function init({ MeasurementService, DisplaySetService, UIDialogService, - ViewportService, } = servicesManager.services; const metadataProvider = OHIF.classes.MetadataProvider; @@ -62,14 +63,14 @@ export default async function init({ prefetch: 50, }; - initWADOImageLoader(UserAuthenticationService); + initWADOImageLoader(UserAuthenticationService, appConfig); // Register the cornerstone-tools-measurement-tool /* Measurement Service */ const measurementServiceSource = connectToolsToMeasurementService( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ); const _getDefaultPosition = event => ({ @@ -125,7 +126,9 @@ export default async function init({ const uid = annotationUID; // Sync'd w/ Measurement Service if (uid) { - measurementServiceSource.remove(uid, { element: item.element }); + measurementServiceSource.remove(uid, { + element: item.element, + }); } CONTEXT_MENU_OPEN = false; }, @@ -197,18 +200,20 @@ export default async function init({ function elementEnabledHandler(evt) { const { viewportId, element } = evt.detail; - const viewportInfo = ViewportService.getViewportInfoById(viewportId); + const viewportInfo = Cornerstone3DViewportService.getViewportInfoById( + viewportId + ); const viewportIndex = viewportInfo.getViewportIndex(); setEnabledElement(viewportIndex, element); - // const volumeUID = ViewportService.getVolumeUIDsForViewportUID(viewportId); + // const volumeUID = Cornerstone3DViewportService.getVolumeUIDsForViewportUID(viewportId); const renderingEngineId = viewportInfo.getRenderingEngineId(); const toolGroupId = viewportInfo.getToolGroupId(); ToolGroupService.addToolGroupViewport( - toolGroupId, viewportId, - renderingEngineId + renderingEngineId, + toolGroupId ); element.addEventListener( @@ -220,7 +225,9 @@ export default async function init({ function elementDisabledHandler(evt) { const { viewportId } = evt.detail; - const viewportInfo = ViewportService.getViewportInfoById(viewportId); + const viewportInfo = Cornerstone3DViewportService.getViewportInfoById( + viewportId + ); ToolGroupService.disable(viewportInfo); } diff --git a/extensions/cornerstone-3d/src/initMeasurementService.js b/extensions/cornerstone-3d/src/initMeasurementService.js index b7f0da94f4e..60d42250948 100644 --- a/extensions/cornerstone-3d/src/initMeasurementService.js +++ b/extensions/cornerstone-3d/src/initMeasurementService.js @@ -3,7 +3,7 @@ import { Enums, annotation } from '@cornerstonejs/tools'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; -const { getAnnotation, removeAnnotation } = annotation.state; +const { removeAnnotation } = annotation.state; const csToolsEvents = Enums.Events; @@ -12,7 +12,7 @@ const CORNERSTONE_TOOLS_3D_SOURCE_NAME = 'CornerstoneTools3D'; const initMeasurementService = ( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ) => { /* Initialization */ const { @@ -23,7 +23,7 @@ const initMeasurementService = ( } = measurementServiceMappingsFactory( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ); const csTools3DVer1MeasurementSource = MeasurementService.createSource( CORNERSTONE_TOOLS_3D_SOURCE_NAME, @@ -69,16 +69,16 @@ const initMeasurementService = ( const connectToolsToMeasurementService = ( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ) => { const csTools3DVer1MeasurementSource = initMeasurementService( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ); connectMeasurementServiceToTools( MeasurementService, - ViewportService, + Cornerstone3DViewportService, csTools3DVer1MeasurementSource ); const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource; @@ -86,31 +86,8 @@ const connectToolsToMeasurementService = ( /* Measurement Service Events */ eventTarget.addEventListener(elementEnabledEvt, evt => { - function addMeasurement(csToolsEvent) { - try { - const evtDetail = csToolsEvent.detail; - const { annotation } = evtDetail; - const { - metadata: { toolName }, - annotationUID, - } = annotation; - - // setting the evtDetail to be the annotation UID in order for measurement service to - // NOT creates its own measurementUID. Todo: this should be rethought - // when we implement the architecture where a measurement can have more than one annotation. - evtDetail.uid = annotationUID; - annotationToMeasurement(toolName, evtDetail); - } catch (error) { - console.warn('Failed to add measurement:', error); - } - } - function updateMeasurement(csToolsEvent) { try { - if (!csToolsEvent.detail.annotation) { - return; - } - const evtDetail = csToolsEvent.detail; const { annotation: { metadata, annotationUID }, @@ -132,16 +109,20 @@ const connectToolsToMeasurementService = ( */ function removeMeasurement(csToolsEvent) { try { - if (csToolsEvent.detail.toolData.metadata.toolDataUID) { - // check if measurement service has such tool id - const id = csToolsEvent.detail.toolData.metadata.toolDataUID; + try { + const evtDetail = csToolsEvent.detail; + const { + annotation: { annotationUID }, + } = evtDetail; - const measurement = MeasurementService.getMeasurement(id); + const measurement = MeasurementService.getMeasurement(annotationUID); if (measurement) { console.log('~~ removeEvt', csToolsEvent); - remove(id); + remove(annotationUID, evtDetail); } + } catch (error) { + console.warn('Failed to update measurement:', error); } } catch (error) { console.warn('Failed to remove measurement:', error); @@ -154,7 +135,7 @@ const connectToolsToMeasurementService = ( const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED; const removedEvt = csToolsEvents.ANNOTATION_REMOVED; - eventTarget.addEventListener(addedEvt, addMeasurement); + eventTarget.addEventListener(addedEvt, updateMeasurement); eventTarget.addEventListener(updatedEvt, updateMeasurement); eventTarget.addEventListener(removedEvt, removeMeasurement); }); @@ -164,7 +145,7 @@ const connectToolsToMeasurementService = ( const connectMeasurementServiceToTools = ( MeasurementService, - ViewportService, + Cornerstone3DViewportService, measurementSource ) => { const { @@ -174,8 +155,26 @@ const connectMeasurementServiceToTools = ( RAW_MEASUREMENT_ADDED, } = MeasurementService.EVENTS; - MeasurementService.subscribe(MEASUREMENTS_CLEARED, () => { - // Todo: handle all measurements cleared + const csTools3DVer1MeasurementSource = MeasurementService.getSource( + CORNERSTONE_TOOLS_3D_SOURCE_NAME, + '1' + ); + + const { measurementToAnnotation } = csTools3DVer1MeasurementSource; + + MeasurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => { + if (!Object.keys(measurements).length) { + return; + } + + for (const measurement of Object.values(measurements)) { + const { uid, source } = measurement; + if (source.name !== CORNERSTONE_TOOLS_3D_SOURCE_NAME) { + continue; + } + + removeAnnotation(uid); + } }); MeasurementService.subscribe( @@ -191,14 +190,8 @@ const connectMeasurementServiceToTools = ( return; } - const { id, label } = measurement; - const toolData = getAnnotation(id); - - if (toolData) { - if ('label' in toolData.metadata) { - toolData.metadata.label = label; - } - } + const annotationType = measurement.metadata.toolName; + measurementToAnnotation(annotationType, measurement); } ); @@ -215,12 +208,15 @@ const connectMeasurementServiceToTools = ( MeasurementService.subscribe( MEASUREMENT_REMOVED, - ({ source, measurement: removedMeasurementId, element }) => { + ({ source, measurement: removedMeasurementId }) => { if (source.name !== CORNERSTONE_TOOLS_3D_SOURCE_NAME) { return; } - removeAnnotation(element, removedMeasurementId); - ViewportService.getRenderingEngine().render(); + removeAnnotation(removedMeasurementId); + const renderingEngine = Cornerstone3DViewportService.getRenderingEngine(); + // Note: We could do a better job by triggering the render on the + // viewport itself, but the removeAnnotation does not include that info... + renderingEngine.render(); } ); }; diff --git a/extensions/cornerstone-3d/src/initWADOImageLoader.js b/extensions/cornerstone-3d/src/initWADOImageLoader.js index 6c8302b01a0..5800c4c40d9 100644 --- a/extensions/cornerstone-3d/src/initWADOImageLoader.js +++ b/extensions/cornerstone-3d/src/initWADOImageLoader.js @@ -9,9 +9,12 @@ const { registerVolumeLoader } = volumeLoader; let initialized = false; -function initWebWorkers() { +function initWebWorkers(appConfig) { const config = { - maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1), + maxWebWorkers: Math.min( + Math.max(navigator.hardwareConcurrency - 1, 1), + appConfig.maxNumberOfWebWorkers + ), startWebWorkersOnDemand: true, taskConfiguration: { decodeTask: { @@ -28,7 +31,10 @@ function initWebWorkers() { } } -export default function initWADOImageLoader(UserAuthenticationService) { +export default function initWADOImageLoader( + UserAuthenticationService, + appConfig +) { cornerstoneWADOImageLoader.external.cornerstone = cornerstone3D; cornerstoneWADOImageLoader.external.dicomParser = dicomParser; @@ -71,5 +77,5 @@ export default function initWADOImageLoader(UserAuthenticationService) { }, }); - initWebWorkers(); + initWebWorkers(appConfig); } diff --git a/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts b/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts index 3d5fb55bc78..3380766aca8 100644 --- a/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts +++ b/extensions/cornerstone-3d/src/services/ToolGroupService/ToolGroupService.ts @@ -54,6 +54,15 @@ export default class ToolGroupService { return ToolGroupManager.getToolGroupForViewport(viewportId); } + public getActiveToolForViewport(viewportId: string): string { + const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId); + if (!toolGroup) { + return null; + } + + return toolGroup.getActivePrimaryMouseButtonTool(); + } + public destroy() { ToolGroupManager.destroy(); this.toolGroupIds = new Set(); @@ -78,22 +87,24 @@ export default class ToolGroupService { } public addToolGroupViewport( - toolGroupId: string, viewportId: string, - renderingEngineId: string + renderingEngineId: string, + toolGroupId?: string ): void { - const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); - if (!toolGroup) { - throw new Error(`ToolGroup ${toolGroupId} does not exist`); - } - - toolGroup.addViewport(viewportId, renderingEngineId); + if (!toolGroupId) { + // If toolGroupId is not provided, add the viewport to all toolGroups + const toolGroups = ToolGroupManager.getAllToolGroups(); + toolGroups.forEach(toolGroup => { + toolGroup.addViewport(viewportId, renderingEngineId); + }); + } else { + const toolGroup = ToolGroupManager.getToolGroup(toolGroupId); + if (!toolGroup) { + throw new Error(`ToolGroup ${toolGroupId} does not exist`); + } - // If toolGroupId is not provided, add the viewport to all toolGroups - const toolGroups = ToolGroupManager.getAllToolGroups(); - toolGroups.forEach(toolGroup => { toolGroup.addViewport(viewportId, renderingEngineId); - }); + } this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId }); } @@ -101,7 +112,7 @@ export default class ToolGroupService { public createToolGroup( toolGroupId: string, tools: Array, - configs: any + configs: any = {} ): Types.IToolGroup { // check if the toolGroup already exists if (this.getToolGroup(toolGroupId)) { diff --git a/extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts similarity index 73% rename from extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts rename to extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts index a67c58b1c97..d9cad7e3cba 100644 --- a/extensions/cornerstone-3d/src/services/ViewportService/ViewportService.ts +++ b/extensions/cornerstone-3d/src/services/ViewportService/Cornerstone3DViewportService.ts @@ -8,11 +8,7 @@ import { } from '@cornerstonejs/core'; import { IViewportService } from './IViewportService'; import { RENDERING_ENGINE_ID } from './constants'; -import ViewportInfo, { - ViewportOptions, - DisplaySet, - DisplaySetOptions, -} from './Viewport'; +import ViewportInfo, { ViewportOptions, DisplaySetOptions } from './Viewport'; const EVENTS = { VIEWPORT_INFO_CREATED: @@ -23,9 +19,7 @@ const EVENTS = { * Handles cornerstone-3D viewport logic including enabling, disabling, and * updating the viewport. */ -class ViewportService implements IViewportService { - servicesManager: unknown; - HangingProtocolService: unknown; +class Cornerstone3DViewportService implements IViewportService { renderingEngine: Types.IRenderingEngine | null; viewportsInfo: Map; viewportGridResizeObserver: ResizeObserver | null; @@ -41,8 +35,7 @@ class ViewportService implements IViewportService { resizeRefreshRateMs: 200; resizeRefreshMode: 'debounce'; - constructor(servicesManager) { - this.servicesManager = servicesManager; + constructor() { this.renderingEngine = null; this.viewportGridResizeObserver = null; this.viewportsInfo = new Map(); @@ -51,8 +44,6 @@ class ViewportService implements IViewportService { this.EVENTS = EVENTS; Object.assign(this, pubSubServiceInterface); // - const { HangingProtocolService } = servicesManager.services; - this.HangingProtocolService = HangingProtocolService; } /** @@ -91,8 +82,9 @@ class ViewportService implements IViewportService { */ public resize() { const immediate = true; - const resetPanZoomForViewPlane = false; - this.renderingEngine.resize(immediate, resetPanZoomForViewPlane); + const resetPan = false; + const resetZoom = false; + this.renderingEngine.resize(immediate, resetPan, resetZoom); this.renderingEngine.render(); } @@ -136,32 +128,36 @@ class ViewportService implements IViewportService { */ public setViewportDisplaySets( viewportIndex: number, - displaySets: unknown[], + viewportData: unknown, viewportOptions: ViewportOptions, - displaySetOptions: unknown[], - dataSource: unknown - ) { + displaySetOptions: unknown[] + ): void { const renderingEngine = this.getRenderingEngine(); const viewportInfo = this.viewportsInfo.get(viewportIndex); viewportInfo.setRenderingEngineId(renderingEngine.id); - const currentViewportOptions = viewportInfo.getViewportOptions(); // If new viewportOptions are provided and have keys that are not in the // current viewportOptions, then we need to update the viewportOptions, // else we inherit the current viewportOptions. + const currentViewportOptions = viewportInfo.getViewportOptions(); + let viewportOptionsToUse = currentViewportOptions; if (Object.keys(viewportOptions)) { - const newViewportOptions = { + viewportOptionsToUse = { ...currentViewportOptions, ...viewportOptions, }; - viewportInfo.setViewportOptions(newViewportOptions); - } else { - viewportInfo.setViewportOptions(currentViewportOptions); } + viewportInfo.setViewportOptions(viewportOptionsToUse); - // Todo: handle changed displaySetOptions - - viewportInfo.setDisplaySets(displaySets, displaySetOptions); + const currentDisplaySetOptions = viewportInfo.getDisplaySetOptions(); + let displaySetOptionsToUse = currentDisplaySetOptions; + if (displaySetOptions?.length) { + displaySetOptionsToUse = [ + ...(currentDisplaySetOptions ?? []), + ...displaySetOptions, + ]; + } + viewportInfo.setDisplaySetOptions(displaySetOptionsToUse); this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo); @@ -184,13 +180,24 @@ class ViewportService implements IViewportService { renderingEngine.enableElement(viewportInput); this._setDisplaySets( viewportId, - displaySets, - viewportOptions, - displaySetOptions, - dataSource + viewportData, + viewportOptionsToUse, + displaySetOptionsToUse ); } + public getCornerstone3DViewport(viewportId: string): StackViewport | null { + const viewportInfo = this.getViewportInfoById(viewportId); + + if (!viewportInfo) { + return null; + } + + const viewport = this.renderingEngine.getViewport(viewportId); + + return viewport; + } + /** * Returns the viewportIndex for the provided viewportId * @param {string} viewportId - the viewportId @@ -210,30 +217,40 @@ class ViewportService implements IViewportService { return null; } - _setStackViewport(viewport, displaySet, displaySetOptions, dataSource) { - const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); - viewport.setStack(imageIds).then(() => { + _setStackViewport(viewport, viewportData, displaySetOptions) { + const { imageIds, initialImageIdIndex } = viewportData.stack; + // Todo: handle fusion stack when it is implemented + const { voi, voiInverted } = displaySetOptions[0]; + + const properties = {}; + if (Array.isArray(voi)) { + const { lower, upper } = csUtils.windowLevel.toLowHighRange( + voi[0], + voi[1] + ); + properties.voiRange = { lower, upper }; + } + + if (voiInverted !== undefined) { + properties.invert = voiInverted; + } + + viewport.setStack(imageIds, initialImageIdIndex).then(() => { + viewport.setProperties(properties); csUtils.prefetchStack(imageIds); }); } _setDisplaySets( viewportId: string, - displaySets: DisplaySet[], + viewportData: unknown, viewportOptions: ViewportOptions, - displaySetOptions: DisplaySetOptions, - dataSource: unknown - ) { + displaySetOptions: DisplaySetOptions + ): void { const viewport = this.renderingEngine.getViewport(viewportId); if (viewport instanceof StackViewport) { - // Todo: No fusion on StackViewport Yet - this._setStackViewport( - viewport, - displaySets[0], - displaySetOptions[0], - dataSource - ); + this._setStackViewport(viewport, viewportData, displaySetOptions); } else { throw new Error('Unsupported viewport type'); } @@ -249,4 +266,4 @@ class ViewportService implements IViewportService { } } -export default ViewportService; +export default new Cornerstone3DViewportService(); diff --git a/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts b/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts index d08c742be07..ac39cd1da8a 100644 --- a/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone-3d/src/services/ViewportService/Viewport.ts @@ -1,4 +1,4 @@ -import { Types, Enums, CONSTANTS } from '@cornerstonejs/core'; +import { Types, Enums } from '@cornerstonejs/core'; export type ViewportOptions = { viewportType: Enums.ViewportType; @@ -23,7 +23,6 @@ class ViewportInfo { private element: HTMLDivElement; private viewportOptions: ViewportOptions; private displaySetOptions: Array; - private displaySets: Array; private renderingEngineId: string; constructor(viewportIndex: number) { @@ -85,18 +84,6 @@ class ViewportInfo { return this.displaySetOptions; } - public setDisplaySets( - displaySets: Array, - displaySetOptions: Array - ): void { - this.displaySets = displaySets; - this.setDisplaySetOptions(displaySetOptions); - } - - public getDisplaySets(): Array { - return this.displaySets; - } - private makeViewportId(viewportIndex: number): void { const viewportId = `viewport-${viewportIndex}`; this.setViewportId(viewportId); diff --git a/extensions/cornerstone-3d/src/services/ViewportService/index.js b/extensions/cornerstone-3d/src/services/ViewportService/index.js deleted file mode 100644 index 1097961542b..00000000000 --- a/extensions/cornerstone-3d/src/services/ViewportService/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import ViewportService from './ViewportService'; - -export default function ExtendedViewportService(serviceManager) { - return { - name: 'ViewportService', - create: ({ configuration = {} }) => { - return new ViewportService(serviceManager); - }, - }; -} diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js index d18fbdf7f24..fbbce0efaf1 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Bidirectional.js @@ -1,13 +1,29 @@ +import { annotation } from '@cornerstonejs/tools'; + import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; const Bidirectional = { - toAnnotation: (measurement, definition) => {}, + // Currently we only update the labels + toAnnotation: measurement => { + const annotationUID = measurement.uid; + const cornerstone3DAnnotation = annotation.state.getAnnotation( + annotationUID + ); + + if (!cornerstone3DAnnotation) { + return; + } + + if (cornerstone3DAnnotation.data.label !== measurement.label) { + cornerstone3DAnnotation.data.label = measurement.label; + } + }, toMeasurement: ( csToolsEventDetail, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, getValueTypeFromToolType ) => { const { annotation, viewportId } = csToolsEventDetail; @@ -31,7 +47,7 @@ const Bidirectional = { StudyInstanceUID, } = getSOPInstanceAttributes( referencedImageId, - ViewportService, + Cornerstone3DViewportService, viewportId ); diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js index 23b57507e61..57ab5b46ddf 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/EllipticalROI.js @@ -1,14 +1,30 @@ +import { annotation } from '@cornerstonejs/tools'; + import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import getModalityUnit from './utils/getModalityUnit'; import { utils } from '@ohif/core'; const EllipticalROI = { - toAnnotation: (measurement, definition) => {}, + // Currently we only update the labels + toAnnotation: measurement => { + const annotationUID = measurement.uid; + const cornerstone3DAnnotation = annotation.state.getAnnotation( + annotationUID + ); + + if (!cornerstone3DAnnotation) { + return; + } + + if (cornerstone3DAnnotation.data.label !== measurement.label) { + cornerstone3DAnnotation.data.label = measurement.label; + } + }, toMeasurement: ( csToolsEventDetail, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, getValueTypeFromToolType ) => { const { annotation, viewportId } = csToolsEventDetail; @@ -32,7 +48,7 @@ const EllipticalROI = { StudyInstanceUID, } = getSOPInstanceAttributes( referencedImageId, - ViewportService, + Cornerstone3DViewportService, viewportId ); @@ -142,6 +158,11 @@ function _getReport(mappedAnnotations, points, FrameOfReferenceUID) { mappedAnnotations.forEach(annotation => { const { mean, stdDev, max, area, unit } = annotation; + + if (!mean || !unit || !max || !area) { + return; + } + columns.push( `max (${unit})`, `mean (${unit})`, @@ -182,8 +203,8 @@ function getDisplayText(mappedAnnotations) { const roundedArea = utils.roundNumber(area, 2); displayText.push(`Area: ${roundedArea} mm2`); - mappedAnnotations.forEach(normalizedAnnotation => { - const { mean, unit, max, SeriesNumber } = normalizedAnnotation; + mappedAnnotations.forEach(mappedAnnotation => { + const { mean, unit, max, SeriesNumber } = mappedAnnotation; const roundedMean = utils.roundNumber(mean, 2); const roundedMax = utils.roundNumber(max, 2); // const roundedStdDev = utils.roundNumber(stdDev, 2); diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js index a9a146a67c7..7be1f7ff584 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/Length.js @@ -1,9 +1,24 @@ +import { annotation } from '@cornerstonejs/tools'; import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import { utils } from '@ohif/core'; const Length = { - toAnnotation: (measurement, definition) => {}, + // Currently we only update the labels + toAnnotation: measurement => { + const annotationUID = measurement.uid; + const cornerstone3DAnnotation = annotation.state.getAnnotation( + annotationUID + ); + + if (!cornerstone3DAnnotation) { + return; + } + + if (cornerstone3DAnnotation.data.label !== measurement.label) { + cornerstone3DAnnotation.data.label = measurement.label; + } + }, /** * Maps cornerstone annotation event data to measurement service format. @@ -14,7 +29,7 @@ const Length = { toMeasurement: ( csToolsEventDetail, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, getValueTypeFromToolType ) => { const { annotation, viewportId } = csToolsEventDetail; @@ -38,7 +53,7 @@ const Length = { StudyInstanceUID, } = getSOPInstanceAttributes( referencedImageId, - ViewportService, + Cornerstone3DViewportService, viewportId ); diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/RectangleROI.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/RectangleROI.js index dff6afaa40e..470081b4941 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/RectangleROI.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/RectangleROI.js @@ -1,14 +1,30 @@ +import { annotation } from '@cornerstonejs/tools'; + import SUPPORTED_TOOLS from './constants/supportedTools'; import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes'; import getModalityUnit from './utils/getModalityUnit'; import { utils } from '@ohif/core'; const RectangleRoi = { - toAnnotation: (measurement, definition) => {}, + // Currently we only update the labels + toAnnotation: measurement => { + const annotationUID = measurement.uid; + const cornerstone3DAnnotation = annotation.state.getAnnotation( + annotationUID + ); + + if (!cornerstone3DAnnotation) { + return; + } + + if (cornerstone3DAnnotation.data.label !== measurement.label) { + cornerstone3DAnnotation.data.label = measurement.label; + } + }, toMeasurement: ( csToolsEventDetail, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, getValueTypeFromToolType ) => { const { annotation, viewportId } = csToolsEventDetail; @@ -32,7 +48,7 @@ const RectangleRoi = { StudyInstanceUID, } = getSOPInstanceAttributes( referencedImageId, - ViewportService, + Cornerstone3DViewportService, viewportId ); @@ -179,11 +195,21 @@ function getDisplayText(mappedAnnotations) { // Area is the same for all series const { area } = mappedAnnotations[0]; + + if (!area) { + return ''; + } + const roundedArea = utils.roundNumber(area, 2); displayText.push(`Area: ${roundedArea} mm2`); - mappedAnnotations.forEach(normalizedAnnotation => { - const { mean, unit, max, SeriesNumber } = normalizedAnnotation; + mappedAnnotations.forEach(mappedAnnotation => { + const { mean, unit, max, SeriesNumber } = mappedAnnotation; + + if (!mean || !unit || !max) { + return; + } + const roundedMean = utils.roundNumber(mean, 2); const roundedMax = utils.roundNumber(max, 2); // const roundedStdDev = utils.roundNumber(stdDev, 2); diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js index a8ff21645ef..7f6cafc1154 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js @@ -6,7 +6,7 @@ import RectangleROI from './RectangleROI'; const measurementServiceMappingsFactory = ( MeasurementService, DisplaySetService, - ViewportService + Cornerstone3DViewportService ) => { /** * Maps measurement service format object to cornerstone annotation object. @@ -44,7 +44,7 @@ const measurementServiceMappingsFactory = ( Length.toMeasurement( csToolsAnnotation, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, _getValueTypeFromToolType ), matchingCriteria: [ @@ -60,7 +60,7 @@ const measurementServiceMappingsFactory = ( Bidirectional.toMeasurement( csToolsAnnotation, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, _getValueTypeFromToolType ), matchingCriteria: [ @@ -82,7 +82,7 @@ const measurementServiceMappingsFactory = ( EllipticalROI.toMeasurement( csToolsAnnotation, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, _getValueTypeFromToolType ), matchingCriteria: [ @@ -97,7 +97,7 @@ const measurementServiceMappingsFactory = ( RectangleROI.toMeasurement( csToolsAnnotation, DisplaySetService, - ViewportService, + Cornerstone3DViewportService, _getValueTypeFromToolType ), matchingCriteria: [ diff --git a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js index 0071a00ae53..0f47897bb20 100644 --- a/extensions/cornerstone-3d/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js +++ b/extensions/cornerstone-3d/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js @@ -13,7 +13,7 @@ import * as cornerstone from '@cornerstonejs/core'; */ export default function getSOPInstanceAttributes( imageId, - ViewportService, + Cornerstone3DViewportService, viewportId ) { if (imageId) { @@ -23,7 +23,7 @@ export default function getSOPInstanceAttributes( // Todo: implement for volume viewports and use the referencedSeriesInstanceUID // if no imageId => measurement is not in the acquisition plane - // const metadata = getUIDFromScene(ViewportService, viewportId); + // const metadata = getUIDFromScene(Cornerstone3DViewportService, viewportId); // if (!metadata) { // throw new Error('Not viewport with imageId found'); @@ -48,8 +48,8 @@ function _getUIDFromImageID(imageId) { }; } -// function getUIDFromScene(ViewportService) { -// const renderingEngine = ViewportService.getRenderingEngine(); +// function getUIDFromScene(Cornerstone3DViewportService) { +// const renderingEngine = Cornerstone3DViewportService.getRenderingEngine(); // const scene = renderingEngine.getScene(sceneUID); // const viewportUIDs = scene.getViewportIds(); diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index e2cb360bbf7..cecd1c8cc35 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -328,6 +328,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) { if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) { seriesSummaryMetadata[instance.SeriesInstanceUID] = { StudyInstanceUID: instance.StudyInstanceUID, + StudyDescription: instance.StudyDescription, SeriesInstanceUID: instance.SeriesInstanceUID, SeriesDescription: instance.SeriesDescription, SeriesNumber: instance.SeriesNumber, diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js index 86c19567743..3d7294edf4d 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js @@ -2,7 +2,6 @@ import dcmjs from 'dcmjs'; import { sortStudySeries, sortingCriteria } from '../utils/sortStudy'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; - /** * Creates an immutable series loader object which loads each series sequentially using the iterator interface * @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load diff --git a/extensions/default/src/ViewerLayout/index.tsx b/extensions/default/src/ViewerLayout/index.tsx index 395f17f7268..063f6ee7dbb 100644 --- a/extensions/default/src/ViewerLayout/index.tsx +++ b/extensions/default/src/ViewerLayout/index.tsx @@ -220,7 +220,7 @@ function ViewerLayout({ )} {/* TOOLBAR + GRID */} -
+
{ + let unsubscribe; + + const activateTool = () => { ToolBarService.recordInteraction({ - groupId: 'primary', - itemId: 'Wwwc', + groupId: 'Wwwc', + itemId: 'WindowLevel', interactionType: 'tool', commands: [ { @@ -106,7 +107,18 @@ function modeFactory({ modeConfiguration }) { }, ], }); - }); + + // We don't need to reset the active tool whenever a viewport is getting + // added to the toolGroup. + unsubscribe(); + }; + + // Since we only have one viewport for the basic cs3d mode and it has + // only one hanging protocol, we can just use the first viewport + ({ unsubscribe } = ToolGroupService.subscribe( + ToolGroupService.EVENTS.VIEWPORT_ADDED, + activateTool + )); ToolBarService.init(extensionManager); ToolBarService.addButtons(toolbarButtons); @@ -116,6 +128,7 @@ function modeFactory({ modeConfiguration }) { 'WindowLevel', 'Pan', 'Layout', + 'MoreTools', ]); }, onModeExit: ({ servicesManager }) => { diff --git a/modes/basic-viewer-cs3d/src/toolbarButtons.js b/modes/basic-viewer-cs3d/src/toolbarButtons.js index d220811a3d7..127ed3bea33 100644 --- a/modes/basic-viewer-cs3d/src/toolbarButtons.js +++ b/modes/basic-viewer-cs3d/src/toolbarButtons.js @@ -54,7 +54,7 @@ function _createWwwcPreset(preset, title, subtitle) { }; } -export default [ +const toolbarButtons = [ // Measurement { id: 'MeasurementTools', @@ -325,22 +325,22 @@ export default [ ], 'Stack Scroll' ), - _createToolButton( - 'Magnify', - 'tool-magnify', - 'Magnify', + _createActionButton( + 'invert', + 'tool-invert', + 'Invert', [ { - commandName: 'setToolActive', - commandOptions: { - toolName: 'Magnify', - }, + commandName: 'invertViewport', + commandOptions: {}, context: 'CORNERSTONE3D', }, ], - 'Magnify' + 'Invert Colors' ), ], }, }, ]; + +export default toolbarButtons; diff --git a/platform/core/src/defaults/hotkeyBindings.js b/platform/core/src/defaults/hotkeyBindings.js index c8fd4d446c7..66acb63fdd4 100644 --- a/platform/core/src/defaults/hotkeyBindings.js +++ b/platform/core/src/defaults/hotkeyBindings.js @@ -3,13 +3,44 @@ import windowLevelPresets from './windowLevelPresets'; /* * Supported Keys: https://craig.is/killing/mice */ -export default [ - { commandName: 'setToolActive', commandOptions: { toolName: 'Zoom' }, label: 'Zoom', keys: ['z'], isEditable: true }, - { commandName: 'scaleUpViewport', label: 'Zoom In', keys: ['+'], isEditable: true }, - { commandName: 'scaleDownViewport', label: 'Zoom Out', keys: ['-'], isEditable: true }, - { commandName: 'fitViewportToWindow', label: 'Zoom to Fit', keys: ['='], isEditable: true }, - { commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'], isEditable: true }, - { commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'], isEditable: true }, +const bindings = [ + { + commandName: 'setToolActive', + commandOptions: { toolName: 'Zoom' }, + label: 'Zoom', + keys: ['z'], + isEditable: true, + }, + { + commandName: 'scaleUpViewport', + label: 'Zoom In', + keys: ['+'], + isEditable: true, + }, + { + commandName: 'scaleDownViewport', + label: 'Zoom Out', + keys: ['-'], + isEditable: true, + }, + { + commandName: 'fitViewportToWindow', + label: 'Zoom to Fit', + keys: ['='], + isEditable: true, + }, + { + commandName: 'rotateViewportCW', + label: 'Rotate Right', + keys: ['r'], + isEditable: true, + }, + { + commandName: 'rotateViewportCCW', + label: 'Rotate Left', + keys: ['l'], + isEditable: true, + }, { commandName: 'flipViewportVertical', label: 'Flip Horizontally', @@ -57,11 +88,36 @@ export default [ keys: ['pagedown'], isEditable: true, }, - { commandName: 'nextImage', label: 'Next Image', keys: ['down'], isEditable: true }, - { commandName: 'previousImage', label: 'Previous Image', keys: ['up'], isEditable: true }, - { commandName: 'firstImage', label: 'First Image', keys: ['home'], isEditable: true }, - { commandName: 'lastImage', label: 'Last Image', keys: ['end'], isEditable: true }, - { commandName: 'resetViewport', label: 'Reset', keys: ['space'], isEditable: true }, + { + commandName: 'nextImage', + label: 'Next Image', + keys: ['down'], + isEditable: true, + }, + { + commandName: 'previousImage', + label: 'Previous Image', + keys: ['up'], + isEditable: true, + }, + { + commandName: 'firstImage', + label: 'First Image', + keys: ['home'], + isEditable: true, + }, + { + commandName: 'lastImage', + label: 'Last Image', + keys: ['end'], + isEditable: true, + }, + { + commandName: 'resetViewport', + label: 'Reset', + keys: ['space'], + isEditable: true, + }, { commandName: 'cancelMeasurement', label: 'Cancel Cornerstone Measurement', @@ -122,3 +178,5 @@ export default [ keys: ['9'], }, ]; + +export default bindings; diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index e02579eb7ca..100a2910e4a 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -205,6 +205,7 @@ const BaseImplementation = { let study = _getStudy(StudyInstanceUID); if (!study) { study = createStudyMetadata(StudyInstanceUID); + study.StudyDescription = seriesSummaryMetadata[0].StudyDescription; _model.studies.push(study); } diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js index e3657e51eca..bb72dc14055 100644 --- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -3,6 +3,7 @@ import createSeriesMetadata from './createSeriesMetadata'; function createStudyMetadata(StudyInstanceUID) { return { StudyInstanceUID, + StudyDescription: '', isLoaded: false, series: [], /** @@ -10,7 +11,7 @@ function createStudyMetadata(StudyInstanceUID) { * @param {object} instance * @returns {bool} true if series were added; false if series already exist */ - addInstanceToSeries: function (instance) { + addInstanceToSeries: function(instance) { const { SeriesInstanceUID } = instance; const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID @@ -29,7 +30,7 @@ function createStudyMetadata(StudyInstanceUID) { * @param {string} instances[].SeriesInstanceUID * @returns {bool} true if series were added; false if series already exist */ - addInstancesToSeries: function (instances) { + addInstancesToSeries: function(instances) { const { SeriesInstanceUID } = instances[0]; const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID @@ -43,7 +44,7 @@ function createStudyMetadata(StudyInstanceUID) { } }, - setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) { + setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) { let existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID ); diff --git a/platform/core/src/services/HangingProtocolService/HPMatcher.js b/platform/core/src/services/HangingProtocolService/HPMatcher.js index 29b0e4a1c58..08b66daaf82 100644 --- a/platform/core/src/services/HangingProtocolService/HPMatcher.js +++ b/platform/core/src/services/HangingProtocolService/HPMatcher.js @@ -1,6 +1,5 @@ import validate from './lib/validator'; - /** * Match a Metadata instance against rules using Validate.js for validation. * @param {InstanceMetadata} metadataInstance Metadata instance object @@ -36,7 +35,13 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => { // Create a single attribute object to be validated, since metadataInstance is an // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata) - const attributeValue = metadataInstance[attribute]; + let attributeValue = metadataInstance[attribute]; + if (attributeValue === undefined) { + if (attribute === 'NumberOfStudyRelatedSeries') { + attributeValue = metadataInstance.series?.length; + } + // Add other computable values such as modalities in study + } const attributeMap = { [attribute]: attributeValue, }; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js index aaecfc90197..7d493f62e38 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js @@ -80,7 +80,7 @@ class HangingProtocolService { addProtocols(protocols) { protocols.forEach(protocol => { if (this.protocols.indexOf(protocol) === -1) { - this.protocols.push(protocol); + this.protocols.push(this._validateProtocol(protocol)); } }); } @@ -227,6 +227,31 @@ class HangingProtocolService { } } + _validateProtocol(protocol) { + protocol.id = protocol.id || protocol.name; + // Automatically compute some number of attributes if they + // aren't present. Makes defining new HPs easier. + protocol.name = protocol.name || protocol.id; + const { stages } = protocol; + + // Generate viewports automatically as required. + stages.forEach(stage => { + if (!stage.viewports) { + stage.viewports = []; + const { rows, columns } = stage.viewportStructure.properties; + + for (let i = 0; i < rows * columns; i++) { + stage.viewports.push({ + viewportOptions: {}, + displaySetOptions: [], + }); + } + } + }); + + return protocol; + } + _setProtocol(protocol) { // TODO: Add proper Protocol class to validate the protocols // which are entered manually @@ -288,10 +313,7 @@ class HangingProtocolService { } this.customImageLoadPerformed = false; - const { layoutType } = stageModel.viewportStructure; - if (!layoutType) { - return; - } + const { type: layoutType } = stageModel.viewportStructure; // Retrieve the properties associated with the current display set's viewport structure template // If no such layout properties exist, stop here. @@ -303,14 +325,14 @@ class HangingProtocolService { const { columns: numCols, rows: numRows, - viewports: viewportsPos, + viewportOptions = [], } = layoutProps; this._broadcastChange(this.EVENTS.NEW_LAYOUT, { layoutType, numRows, numCols, - viewportsPos, + viewportOptions, }); // Matching the displaySets @@ -331,14 +353,25 @@ class HangingProtocolService { // but it is a info to locate the displaySet from the displaySetService let displaySetsInfo = []; viewport.displaySets.forEach(({ id, options: displaySetOptions }) => { - const { SeriesInstanceUID } = this.displaySetMatchDetails.get(id); - - const displaySetInfo = { - SeriesInstanceUID, - displaySetOptions, - }; - - displaySetsInfo.push(displaySetInfo); + const viewportDisplaySet = this.displaySetMatchDetails.get(id); + + if (viewportDisplaySet) { + const { SeriesInstanceUID } = viewportDisplaySet; + + const displaySetInfo = { + SeriesInstanceUID, + displaySetOptions, + }; + + displaySetsInfo.push(displaySetInfo); + } else { + console.warn( + ` + The hanging protocol viewport is requesting to display ${id} displaySet that is not + matched based on the provided criteria (e.g. matching rules). + ` + ); + } }); this.matchDetails[viewportIndex] = { diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index 540f502748f..92185a79f55 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -15,7 +15,7 @@ import pubSubServiceInterface from '../_shared/pubSubServiceInterface'; * Measurement schema * * @typedef {Object} Measurement - * @property {number} id - + * @property {number} uid - * @property {string} SOPInstanceUID - * @property {string} FrameOfReferenceUID - * @property {string} referenceSeriesUID - @@ -167,9 +167,9 @@ class MeasurementService { } /** - * Get specific measurement by its id. + * Get specific measurement by its uid. * - * @param {string} id Id of the measurement + * @param {string} uid measurement uid * @return {Measurement} Measurement instance */ getMeasurement(measurementUID) { @@ -225,8 +225,8 @@ class MeasurementService { return this.measurementToAnnotation(source, annotationType, measurement); }; - source.remove = (id, eventDetails) => { - return this.remove(id, source, eventDetails); + source.remove = (measurementUID, eventDetails) => { + return this.remove(measurementUID, source, eventDetails); }; source.getAnnotation = (annotationType, measurementId) => { @@ -315,7 +315,7 @@ class MeasurementService { * * @param {MeasurementSource} source Measurement source instance * @param {string} annotationType The source annotationType - * @param {string} measurementUID The measurement service measurement id + * @param {string} measurementUID The measurement service measurement uid * @return {Object} Source measurement schema */ getAnnotation(source, annotationType, measurementUID) { @@ -512,7 +512,7 @@ class MeasurementService { * @param {MeasurementSource} source The measurement source instance * @param {string} annotationType The source annotationType * @param {EventDetail} sourceAnnotationEvent for the annotation event - * @return {string} A measurement id + * @return {string} A measurement uid */ annotationToMeasurement(source, annotationType, sourceAnnotationEvent) { if (!this._isValidSource(source)) { @@ -586,19 +586,18 @@ class MeasurementService { }); } - return newMeasurement.id; + return newMeasurement.uid; } /** * Removes a measurement and broadcasts the removed event. * - * @param {string} measurementUID The measurement id + * @param {string} measurementUID The measurement uid * @param {MeasurementSource} source The measurement source instance - * @return {string} The removed measurement id */ remove(measurementUID, source, eventDetails) { if (!measurementUID || !this.measurements[measurementUID]) { - log.warn(`No id provided, or unable to find measurement by id.`); + log.warn(`No uid provided, or unable to find measurement by uid.`); return; } @@ -611,9 +610,11 @@ class MeasurementService { } clearMeasurements() { + // Make a copy of the measurements + const measurements = { ...this.measurements }; this.measurements = {}; this._jumpToMeasurementCache = {}; - this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED); + this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } jumpToMeasurement(viewportIndex, measurementUID) { diff --git a/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css new file mode 100644 index 00000000000..6d1aaf32d05 --- /dev/null +++ b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.css @@ -0,0 +1,106 @@ +.scroll { + height: 100%; + padding: 5px; + position: absolute; + right: 0; + top: 0; +} +.scroll .scroll-holder { + height: calc(100% - 20px); + margin-top: 5px; + position: relative; + width: 12px; +} +.scroll .scroll-holder .imageSlider { + height: 12px; + left: 12px; + padding: 0; + position: absolute; + top: 0; + transform: rotate(90deg); + transform-origin: top left; + -webkit-appearance: none; + background-color: rgba(0, 0, 0, 0); +} +.scroll .scroll-holder .imageSlider:focus { + outline: none; +} +.scroll .scroll-holder .imageSlider::-moz-focus-outer { + border: none; +} +.scroll .scroll-holder .imageSlider::-webkit-slider-runnable-track { + background-color: rgba(0, 0, 0, 0); + border: none; + cursor: pointer; + height: 5px; + z-index: 6; +} +.scroll .scroll-holder .imageSlider::-moz-range-track { + background-color: rgba(0, 0, 0, 0); + border: none; + cursor: pointer; + height: 2px; + z-index: 6; +} +.scroll .scroll-holder .imageSlider::-ms-track { + animate: 0.2s; + background: transparent; + border: none; + border-width: 15px 0; + color: rgba(0, 0, 0, 0); + cursor: pointer; + height: 12px; + width: 100%; +} +.scroll .scroll-holder .imageSlider::-ms-fill-lower { + background: rgba(0, 0, 0, 0); +} +.scroll .scroll-holder .imageSlider::-ms-fill-upper { + background: rgba(0, 0, 0, 0); +} +.scroll .scroll-holder .imageSlider::-webkit-slider-thumb { + -webkit-appearance: none !important; + background-color: #163239; + border: none; + border-radius: 57px; + cursor: -webkit-grab; + height: 12px; + margin-top: -4px; + width: 39px; +} +.scroll .scroll-holder .imageSlider::-webkit-slider-thumb:active { + background-color: #20a5d6; + cursor: -webkit-grabbing; +} +.scroll .scroll-holder .imageSlider::-moz-range-thumb { + background-color: #163239; + border: none; + border-radius: 57px; + cursor: -moz-grab; + height: 12px; + width: 39px; + z-index: 7; +} +.scroll .scroll-holder .imageSlider::-moz-range-thumb:active { + background-color: #20a5d6; + cursor: -moz-grabbing; +} +.scroll .scroll-holder .imageSlider::-ms-thumb { + background-color: #163239; + border: none; + border-radius: 57px; + cursor: ns-resize; + height: 12px; + width: 39px; +} +.scroll .scroll-holder .imageSlider::-ms-thumb:active { + background-color: #20a5d6; +} +.scroll .scroll-holder .imageSlider::-ms-tooltip { + display: none; +} +@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + .imageSlider { + left: 50px; + } +} diff --git a/platform/ui/src/components/ImageScrollbar/ImageScrollbar.tsx b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.tsx new file mode 100644 index 00000000000..f7cb7d8e91c --- /dev/null +++ b/platform/ui/src/components/ImageScrollbar/ImageScrollbar.tsx @@ -0,0 +1,67 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './ImageScrollbar.css'; + +class ImageScrollbar extends PureComponent { + static propTypes = { + value: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + height: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + }; + + render() { + if (this.props.max === 0) { + return null; + } + + this.style = { + width: `${this.props.height}`, + }; + + return ( +
+
+ +
+
+ ); + } + + onChange = event => { + const intValue = parseInt(event.target.value, 10); + this.props.onChange(intValue); + }; + + onKeyDown = event => { + // We don't allow direct keyboard up/down input on the + // image sliders since the natural direction is reversed (0 is at the top) + + // Store the KeyCodes in an object for readability + const keys = { + DOWN: 40, + UP: 38, + }; + + // TODO: Enable scroll down / scroll up without depending on ohif-core + if (event.which === keys.DOWN) { + //OHIF.commands.run('scrollDown'); + event.preventDefault(); + } else if (event.which === keys.UP) { + //OHIF.commands.run('scrollUp'); + event.preventDefault(); + } + }; +} + +export default ImageScrollbar; diff --git a/platform/ui/src/components/ImageScrollbar/index.js b/platform/ui/src/components/ImageScrollbar/index.js new file mode 100644 index 00000000000..026e347ef3f --- /dev/null +++ b/platform/ui/src/components/ImageScrollbar/index.js @@ -0,0 +1,2 @@ +import ImageScrollbar from './ImageScrollbar'; +export default ImageScrollbar; diff --git a/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx b/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx index 1de2d78c37e..79f3ceb2921 100644 --- a/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx +++ b/platform/ui/src/components/ViewportGrid/ViewportGrid.tsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -function ViewportGrid({ numRows, numCols, gridType, children }) { +function ViewportGrid({ numRows, numCols, layoutType, children }) { return (
{children} diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 6b4bb9b87cb..aac18fba4e3 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -61,6 +61,7 @@ import UserPreferences from './UserPreferences'; import HotkeysPreferences from './HotkeysPreferences'; import HotkeyField from './HotkeyField'; import Header from './Header'; +import ImageScrollbar from './ImageScrollbar'; export { AboutModal, @@ -87,6 +88,7 @@ export { InputLabelWrapper, InputMultiSelect, InputText, + ImageScrollbar, Label, LayoutSelector, MeasurementTable, diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx index 08fdc0db0cc..bed9347c0a0 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx @@ -65,8 +65,15 @@ export function ViewportGridProvider({ children, service }) { return { ...state, ...{ viewports }, cachedLayout: null }; } case 'SET_LAYOUT': { - const { numCols, numRows, layoutType, viewportsPos } = action.payload; - const numPanes = viewportsPos ? viewportsPos.length : numCols * numRows; + const { + numCols, + numRows, + layoutType, + viewportOptions, + } = action.payload; + + // If empty viewportOptions, we use numRow and numCols to calculate number of viewports + const numPanes = viewportOptions.length || numRows * numCols; const viewports = state.viewports.slice(); const activeViewportIndex = state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex; @@ -81,8 +88,8 @@ export function ViewportGridProvider({ children, service }) { for (let i = 0; i < numPanes; i++) { let xPos, yPos, w, h; - if (viewportsPos && viewportsPos[i]) { - ({ x: xPos, y: yPos, width: w, height: h } = viewportsPos[i]); + if (viewportOptions && viewportOptions[i]) { + ({ x: xPos, y: yPos, width: w, height: h } = viewportOptions[i]); } else { const { row, col } = unravelIndex(i, numRows, numCols); w = 1 / numCols; @@ -177,14 +184,14 @@ export function ViewportGridProvider({ children, service }) { ); const setLayout = useCallback( - ({ layoutType, numRows, numCols, viewportsPos }) => + ({ layoutType, numRows, numCols, viewportOptions = [] }) => dispatch({ type: 'SET_LAYOUT', payload: { layoutType, numRows, numCols, - viewportsPos, + viewportOptions, }, }), [dispatch] diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index d80db595de9..e6cb88c3e81 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -96,6 +96,7 @@ export { ViewportGrid, ViewportPane, WindowLevelMenuItem, + ImageScrollbar, } from './components'; /** These are mostly used in the docs */ diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index ccad643070f..86161bc7899 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -4,6 +4,7 @@ window.config = { extensions: [], modes: [], showStudyList: true, + maxNumberOfWebWorkers: 3, // filterQueryParam: false, dataSources: [ { diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index 0cd23371dcf..ef1c74a0783 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -10,15 +10,8 @@ import classNames from 'classnames'; function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; const [viewportGrid, viewportGridService] = useViewportGrid(); - const [, setState] = useState({}); - const { - numCols, - numRows, - activeViewportIndex, - viewports, - cachedLayout, - } = viewportGrid; + const { numCols, numRows, activeViewportIndex, viewports } = viewportGrid; // TODO -> Need some way of selecting which displaySets hit the viewports. const { @@ -58,7 +51,7 @@ function ViewerViewportGrid(props) { const { displaySetsInfo, viewportOptions } = matchDetails[i]; const displaySetUIDsToHang = []; - const displaySetOptions = []; + const displaySetUIDsToHangOptions = []; displaySetsInfo.forEach(({ SeriesInstanceUID, displaySetOptions }) => { const matchingDisplaySet = availableDisplaySets.find(ds => { return ds.SeriesInstanceUID === SeriesInstanceUID; @@ -69,7 +62,7 @@ function ViewerViewportGrid(props) { } displaySetUIDsToHang.push(matchingDisplaySet.displaySetInstanceUID); - displaySetOptions.push(displaySetOptions); + displaySetUIDsToHangOptions.push(displaySetOptions); }); if (!displaySetUIDsToHang.length) { @@ -80,7 +73,7 @@ function ViewerViewportGrid(props) { viewportIndex: i, displaySetInstanceUIDs: displaySetUIDsToHang, viewportOptions, - displaySetOptions, + displaySetOptions: displaySetUIDsToHangOptions, }); // During setting displaySets for viewport, we need to update the hanging protocol @@ -101,31 +94,12 @@ function ViewerViewportGrid(props) { useEffect(() => { const { unsubscribe } = HangingProtocolService.subscribe( HangingProtocolService.EVENTS.NEW_LAYOUT, - ({ gridType, numRows, numCols, viewportsPos }) => { + ({ layoutType, numRows, numCols, viewportOptions }) => { viewportGridService.setLayout({ numRows, numCols, - gridType, - viewportsPos, - }); - } - ); - - return () => { - unsubscribe(); - }; - }, [viewports]); - - // Layout change based on hanging protocols - useEffect(() => { - const { unsubscribe } = HangingProtocolService.subscribe( - HangingProtocolService.EVENTS.NEW_LAYOUT, - ({ gridType, numRows, numCols, viewportsPos }) => { - viewportGridService.setLayout({ - numRows, - numCols, - gridType, - viewportsPos, + layoutType, + viewportOptions, }); } ); @@ -149,28 +123,6 @@ function ViewerViewportGrid(props) { }; }, [viewports]); - /** - * Layout Change - */ - // Layout change based on hanging protocols - useEffect(() => { - const { unsubscribe } = HangingProtocolService.subscribe( - HangingProtocolService.EVENTS.NEW_LAYOUT, - ({ gridType, numRows, numCols, viewportsPos }) => { - viewportGridService.setLayout({ - numRows, - numCols, - gridType, - viewportsPos, - }); - } - ); - - return () => { - unsubscribe(); - }; - }, [viewports]); - useEffect(() => { const { unsubscribe } = MeasurementService.subscribe( MeasurementService.EVENTS.JUMP_TO_MEASUREMENT, @@ -344,39 +296,35 @@ function ViewerViewportGrid(props) { // onDoubleClick={() => onDoubleClick(viewportIndex)} viewportPanes[i] = ( -
- -
- -
-
-
+ +
+ ); } diff --git a/yarn.lock b/yarn.lock index 09083aa93ad..b894bff846f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1320,24 +1320,24 @@ integrity sha512-HOMMOLV6xy8O/agNGGvrl0a8DwShpBvWxAzEzv2pqq12d3r5z/3MyIgNA3Oj/8bIBVvvVXxh9RX7rMDRHJdowg== "@cornerstonejs/core@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.6.0.tgz#e9924f4dfe87ffe8c674202eae3e2bb9fe161158" - integrity sha512-XAu7J49c1zE5Dj/X/NKiyZ+lwEQpUfqi3NQGbQ7AcBvqXvuOAm0twO5vZ9qmD2z1Q6ga3a9ruIH7gr6Z/2xDSw== + version "0.6.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/core/-/core-0.6.1.tgz#ac5af33b62b3efa6a3b9408f788250a92a134a78" + integrity sha512-dhWN6kVVPfV6DMddwvurlQNCI4zgSF08/e/dhyHynbbwTz7EaM3amHq3szSb7qiF00hKxu4IAuRsEpKWBCK6WQ== dependencies: detect-gpu "^4.0.7" lodash.clonedeep "4.5.0" "@cornerstonejs/streaming-image-volume-loader@^0.2.13": - version "0.2.19" - resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.2.19.tgz#f7bfdf25659fcf220821134aba4ef685baf9b0d0" - integrity sha512-yCAjL/BXZnLZbeSvulBKkyfKeZm7v33825vGfPL51blJ9kQSvtkTEQ0z844VPftAB8oB6vRGo9022soeBSyiBQ== + version "0.2.20" + resolved "https://registry.yarnpkg.com/@cornerstonejs/streaming-image-volume-loader/-/streaming-image-volume-loader-0.2.20.tgz#71d98f66d3577b513155a7be8f930ea16c24bf45" + integrity sha512-HWzyAyEoDW+xnDZ5GGXik9CNHy0o3gLSwc6Wm9N1AKvT6mEmKPkuq+BT+11IVc7JHgFPv0XqzU2KE/jy7zQ9Tw== dependencies: cornerstone-wado-image-loader "^4.1.2" "@cornerstonejs/tools@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.9.0.tgz#dcef538197c8f66d5359ab826d68cb639cf5f0eb" - integrity sha512-MrSq9fEdlreHE3uffKGJPKRqqnBOcj/hu6yBycMVuVztHYl7oEPxqS3kis0P2doyJWE2SEK78+ty7wNAJxNNZA== + version "0.9.1" + resolved "https://registry.yarnpkg.com/@cornerstonejs/tools/-/tools-0.9.1.tgz#4d680062714572c230502c7140d82400c7870317" + integrity sha512-OXpm7JYEiseP55SDY8qk7cLrJVOjL7bpbX0ntEWZacGa+YWbdJV0H/XIezMKeHejiJvR55WDUIRFxtEzhlxe5Q== dependencies: lodash.clonedeep "4.5.0" @@ -3409,17 +3409,17 @@ dependencies: "@percy/sdk-utils" "^1.0.0-beta.44" -"@percy/logger@1.0.7": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.0.7.tgz#b6cba3cefaf0f3f74a6d9cc5108073fc953b7ed4" - integrity sha512-pYW6i0/M9h2PMDGiAYpRzWBt2DMyafsF6fNnIlNGOukQKlqlcae4dxPQFq6Mu3tVv3JMUfIx9h2lew5WquF95g== +"@percy/logger@1.0.8": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@percy/logger/-/logger-1.0.8.tgz#c30606b39bdbf646fe896056b772a93314fcf409" + integrity sha512-uHIk6YW7iJGcl/y/QHRl1R8t4NavyTZwji6oe1WW0BUGpR4xE6WjzXqSWv6UezfLLvZGV0AI8vrFqD95d8LLXg== "@percy/sdk-utils@^1.0.0-beta.44": - version "1.0.7" - resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.0.7.tgz#aada1fbf3cf16b07b1e256acf7ebf5c4dda571f9" - integrity sha512-02dL6yLUSvVjbHZz2/ATLg2gRJFKeYvyIADsomuFKzSUQ2WW6pDEaRk/pBWcfS5PR/7sQ5/mV5kN389F/kU7lw== + version "1.0.8" + resolved "https://registry.yarnpkg.com/@percy/sdk-utils/-/sdk-utils-1.0.8.tgz#70eba8b0a6b684048f6e6b03fc050edc191af089" + integrity sha512-cA45Wk9c2MFutso9W5uwj2yMbX6RRbVwsmeqwqNZUpVk6V84Tvc2Ci0gPswbc9fCC0OOHIXOq5FZ3920McVeLA== dependencies: - "@percy/logger" "1.0.7" + "@percy/logger" "1.0.8" "@philpl/buble@^0.19.7": version "0.19.7" @@ -4978,9 +4978,9 @@ integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= "@types/lodash@^4.14.53": - version "4.14.181" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d" - integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag== + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== "@types/mdast@^3.0.0": version "3.0.10" @@ -5013,9 +5013,9 @@ form-data "^3.0.0" "@types/node@*", "@types/node@>= 8", "@types/node@^17.0.5": - version "17.0.24" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.24.tgz#20ba1bf69c1b4ab405c7a01e950c4f446b05029f" - integrity sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g== + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" + integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== "@types/node@12.12.50": version "12.12.50" @@ -5218,9 +5218,9 @@ integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== "@types/webpack-env@^1.16.0": - version "1.16.3" - resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" - integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw== + version "1.16.4" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.4.tgz#1f4969042bf76d7ef7b5914f59b3b60073f4e1f4" + integrity sha512-llS8qveOUX3wxHnSykP5hlYFFuMfJ9p5JvIyCiBgp7WTfl6K5ZcyHj8r8JsN/J6QODkAsRRCLIcTuOCu8etkUw== "@types/webpack-sources@*": version "3.2.0" @@ -5660,9 +5660,9 @@ JSONStream@^1.0.4, JSONStream@^1.3.4: through ">=2.2.7 <3" abab@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" - integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== abbrev@1: version "1.1.1" @@ -6937,7 +6937,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.19.1, browserslist@^4.20.2, browserslist@^4.8.3: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.20.2, browserslist@^4.8.3: version "4.20.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== @@ -8090,17 +8090,17 @@ copy-webpack-plugin@^9.0.0, copy-webpack-plugin@^9.0.1: serialize-javascript "^6.0.0" core-js-compat@^3.20.2, core-js-compat@^3.21.0, core-js-compat@^3.8.1: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" - integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== + version "3.22.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.0.tgz#7ce17ab57c378be2c717c7c8ed8f82a50a25b3e4" + integrity sha512-WwA7xbfRGrk8BGaaHlakauVXrlYmAIkk8PNGb1FDQS+Rbrewc3pgFfwJFRw6psmJVAll7Px9UHRYE16oRQnwAQ== dependencies: - browserslist "^4.19.1" + browserslist "^4.20.2" semver "7.0.0" core-js-pure@^3.20.2, core-js-pure@^3.8.1, core-js-pure@^3.8.2: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51" - integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ== + version "3.22.0" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.0.tgz#0eaa54b6d1f4ebb4d19976bb4916dfad149a3747" + integrity sha512-ylOC9nVy0ak1N+fPIZj00umoZHgUVqmucklP5RT5N+vJof38klKn8Ze6KGyvchdClvEBr6LcQqJpI216LUMqYA== core-js@^2.5.7, core-js@^2.6.5: version "2.6.12" @@ -8108,9 +8108,9 @@ core-js@^2.5.7, core-js@^2.6.5: integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== core-js@^3.0.4, core-js@^3.14.0, core-js@^3.16.1, core-js@^3.2.1, core-js@^3.6.5, core-js@^3.8.2, core-js@^3.8.3, core-js@^3.9.1: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94" - integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig== + version "3.22.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.0.tgz#b52007870c5e091517352e833b77f0b2d2b259f3" + integrity sha512-8h9jBweRjMiY+ORO7bdWSeWfHhLPO7whobj7Z2Bl0IDo00C228EdGgH7FE4jGumbEjzcFfkfW8bXgdkEDhnwHQ== core-util-is@1.0.2: version "1.0.2" @@ -8834,9 +8834,9 @@ dateformat@^3.0.0: integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== dayjs@^1.9.3: - version "1.11.0" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805" - integrity sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug== + version "1.11.1" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.1.tgz#90b33a3dda3417258d48ad2771b415def6545eb0" + integrity sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== dcmjs@0.16.1: version "0.16.1" @@ -9000,11 +9000,12 @@ define-lazy-prop@^2.0.0: integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== dependencies: - object-keys "^1.0.12" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" define-property@^0.2.5: version "0.2.5" @@ -9127,9 +9128,9 @@ detab@2.0.4: repeat-string "^1.5.4" detect-gpu@^4.0.16, detect-gpu@^4.0.7: - version "4.0.17" - resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-4.0.17.tgz#e65a14f327e1be78c6861e1224d9b96843120629" - integrity sha512-e5G1RSOcKEVeIGJ76RFg8q6q9Ol2BgU5feu+1XGKOU9XWg5f+Oh5zbUVmERm4h0RueT9kepCchFawDCnT7gbFA== + version "4.0.18" + resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-4.0.18.tgz#5f367d9fea3e0570ccbe47fec920fa03f5705b4d" + integrity sha512-dqBIrt6EJ7YfSWN9d1HylWI/2j7YbnDNdU4jH4zm7/l4XJQhsEk77WrNEYfp9v8OWPmho3hqv54up1zafwxgzw== dependencies: webgl-constants "^1.1.1" @@ -9519,9 +9520,9 @@ ejs@^3.1.6: jake "^10.6.1" electron-to-chromium@^1.3.564, electron-to-chromium@^1.4.84: - version "1.4.107" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.107.tgz#564257014ab14033b4403a309c813123c58a3fb9" - integrity sha512-Huen6taaVrUrSy8o7mGStByba8PfOWWluHNxSHGBrCgEdFVLtvdQDBr9LBCF9Uci8SYxh28QNNMO0oC17wbGAg== + version "1.4.113" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.113.tgz#b3425c086e2f4fc31e9e53a724c6f239e3adb8b9" + integrity sha512-s30WKxp27F3bBH6fA07FYL2Xm/FYnYrKpMjHr3XVCTUb9anAyZn/BeZfPWgTZGAbJeT4NxNwISSbLcYZvggPMA== elegant-spinner@^1.0.1: version "1.0.1" @@ -10974,9 +10975,9 @@ fs-extra@^0.30.0: rimraf "^2.2.8" fs-extra@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -11605,6 +11606,13 @@ has-glob@^1.0.0: dependencies: is-glob "^3.0.0" +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -15062,9 +15070,9 @@ moment@2.24.0: integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== moment@>=1.6.0, moment@^2.24.0, moment@^2.29.1: - version "2.29.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" - integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== + version "2.29.3" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" + integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== moo-color@^1.0.2: version "1.0.3" @@ -15168,9 +15176,9 @@ nan@^2.12.1: integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanoid@^3.1.23, nanoid@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557" - integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA== + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== nanomatch@^1.2.9: version "1.2.13" @@ -15649,7 +15657,7 @@ object-is@^1.0.1, object-is@^1.1.2: call-bind "^1.0.2" define-properties "^1.1.3" -object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== @@ -17553,7 +17561,12 @@ prism-react-renderer@^1.2.1: resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.1.tgz#88fc9d0df6bed06ca2b9097421349f8c2f24e30d" integrity sha512-xUeDMEz074d0zc5y6rxiMp/dlC7C+5IDDlaEUlcBOFE2wddz7hz5PNupb087mPwTt7T9BrFmewObfCBuf/LKwQ== -prismjs@^1.21.0, prismjs@^1.23.0, prismjs@~1.27.0: +prismjs@^1.21.0, prismjs@^1.23.0: + version "1.28.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6" + integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw== + +prismjs@~1.27.0: version "1.27.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== @@ -18322,22 +18335,22 @@ react-router-dom@6.3.0, react-router-dom@^6.0.0: react-router "6.3.0" react-router-dom@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" - integrity sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ== + version "5.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.1.tgz#0151baf2365c5fcd8493f6ec9b9b31f34d0f8ae1" + integrity sha512-f0pj/gMAbv9e8gahTmCEY20oFhxhrmHwYeIwH5EO5xu0qme+wXtsdB8YfUOAZzUz4VaXmb58m3ceiLtjMhqYmQ== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.2.1" + react-router "5.3.1" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.2.1, react-router@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.1.tgz#4d2e4e9d5ae9425091845b8dbc6d9d276239774d" - integrity sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ== +react-router@5.3.1, react-router@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.1.tgz#b13e84a016c79b9e80dde123ca4112c4f117e3cf" + integrity sha512-v+zwjqb7bakqgF+wMVKlAPTca/cEmPOvQ9zt7gpSNyPXau1+0qvuYZ5BWzzNDP1y6s15zDwgb9rPN63+SIniRQ== dependencies: "@babel/runtime" "^7.12.13" history "^4.9.0" @@ -18687,9 +18700,9 @@ reduce-css-calc@^2.1.8: postcss-value-parser "^3.3.0" redux@^4.0.5: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== dependencies: "@babel/runtime" "^7.9.2" @@ -18747,12 +18760,13 @@ regex-not@^1.0.0, regex-not@^1.0.2: safe-regex "^1.1.0" regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.2.tgz#bf635117a2f4b755595ebb0c0ee2d2a49b2084db" - integrity sha512-Ynz8fTQW5/1elh+jWU2EDDzeoNbD0OQ0R+D1VJU5ATOkUaro4A9YEkdN2ODQl/8UQFPPpZNw91fOcLFamM7Pww== + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" + functions-have-names "^1.2.2" regexpp@^3.0.0, regexpp@^3.1.0: version "3.2.0" @@ -19239,9 +19253,9 @@ rollup-plugin-terser@^7.0.0: terser "^5.0.0" rollup@^2.43.1: - version "2.70.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.1.tgz#824b1f1f879ea396db30b0fc3ae8d2fead93523e" - integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== + version "2.70.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.70.2.tgz#808d206a8851628a065097b7ba2053bd83ba0c0d" + integrity sha512-EitogNZnfku65I1DD5Mxe8JYRUCy0hkK5X84IlDtUs+O6JRMpRciXTzyCUuX11b5L5pvjH+OmFXiQ3XjabcXgg== optionalDependencies: fsevents "~2.3.2" @@ -20125,9 +20139,9 @@ std-env@^2.2.1: ci-info "^3.1.1" std-env@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.0.1.tgz#bc4cbc0e438610197e34c2d79c3df30b491f5182" - integrity sha512-mC1Ps9l77/97qeOZc+HrOL7TIaOboHqMZ24dGVQrlxFcpPpfCHpH+qfUT7Dz+6mlG8+JPA1KfBQo19iC/+Ngcw== + version "3.1.1" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.1.1.tgz#1f19c4d3f6278c52efd08a94574a2a8d32b7d092" + integrity sha512-/c645XdExBypL01TpFKiG/3RAa/Qmu+zRi0MwAmrdEkwHNuN0ebo8ccAXBBDa5Z0QOJgBskUIbuCK91x0sCVEw== stealthy-require@^1.1.1: version "1.1.1"