fix(bugs): and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs (#4049)

This commit is contained in:
Alireza 2024-04-17 21:59:47 -04:00 committed by GitHub
parent 058da93863
commit da7c1a5d8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 173 additions and 118 deletions

View File

@ -4,7 +4,7 @@ import PanelGenerateImage from './PanelGenerateImage';
function DynamicDataPanel({ servicesManager, commandsManager }) {
return (
<div
className="flex flex-auto flex-col text-white"
className="flex flex-col text-white"
data-cy={'dynamic-volume-panel'}
>
<PanelGenerateImage

View File

@ -61,6 +61,24 @@ const cornerstoneExtension: Types.Extensions.Extension = {
*/
id,
onModeEnter: ({ servicesManager }): void => {
const { cornerstoneViewportService, toolbarService, segmentationService } =
servicesManager.services;
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
]);
toolbarService.registerEventForToolbarUpdate(cornerstone.eventTarget, [
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
]);
},
onModeExit: ({ servicesManager }): void => {
const { cineService } = servicesManager.services;
// Empty out the image load and retrieval pools to prevent memory leaks

View File

@ -99,26 +99,10 @@ export default async function init({
uiNotificationService,
cornerstoneViewportService,
hangingProtocolService,
toolbarService,
viewportGridService,
stateSyncService,
segmentationService,
} = servicesManager.services as CornerstoneServices;
toolbarService.registerEventForToolbarUpdate(cornerstoneViewportService, [
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
]);
toolbarService.registerEventForToolbarUpdate(segmentationService, [
segmentationService.EVENTS.SEGMENTATION_ADDED,
segmentationService.EVENTS.SEGMENTATION_REMOVED,
segmentationService.EVENTS.SEGMENTATION_UPDATED,
]);
toolbarService.registerEventForToolbarUpdate(eventTarget, [
cornerstoneTools.Enums.Events.TOOL_ACTIVATED,
]);
window.services = servicesManager.services;
window.extensionManager = extensionManager;
window.commandsManager = commandsManager;

View File

@ -98,7 +98,21 @@ class CornerstoneCacheService {
const volume = cs3DCache.getVolume(volumeId);
if (volume) {
cs3DCache.removeVolumeLoadObject(volumeId);
if (volume.imageIds) {
// also for each imageId in the volume, remove the imageId from the cache
// since that will hold the old metadata as well
volume.imageIds.forEach(imageId => {
if (cs3DCache.getImageLoadObject(imageId)) {
cs3DCache.removeImageLoadObject(imageId);
}
});
}
// this shouldn't be via removeVolumeLoadObject, since that will
// remove the texture as well, but here we really just need a remove
// from registry so that we load it again
cs3DCache._volumeCache.delete(volumeId);
this.volumeImageIds.delete(volumeId);
}

View File

@ -1147,6 +1147,10 @@ class SegmentationService extends PubSubService {
displaySet.isHydrated = isHydrated;
displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false);
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
segmentation: this.getSegmentation(displaySetUID),
});
}
private _highlightLabelmap(

View File

@ -2,6 +2,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
const CircleROI = {
toAnnotation: measurement => {},
@ -193,11 +194,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) {
const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${getDisplayUnit(unit)}</small> `;
}
const maxStr = getStatisticDisplayString(max, unit, 'max');
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {

View File

@ -2,6 +2,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
const EllipticalROI = {
toAnnotation: measurement => {},
@ -192,12 +193,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) {
const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${getDisplayUnit(unit)}</small> `;
}
const maxStr = getStatisticDisplayString(max, unit, 'max');
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {
displayText.push(str);

View File

@ -2,6 +2,7 @@ import SUPPORTED_TOOLS from './constants/supportedTools';
import { getDisplayUnit } from './utils';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { utils } from '@ohif/core';
import { getStatisticDisplayString } from './utils/getValueDisplayString';
const RectangleROI = {
toAnnotation: measurement => {},
@ -193,11 +194,7 @@ function getDisplayText(mappedAnnotations, displaySet, customizationService) {
mappedAnnotations.forEach(mappedAnnotation => {
const { unit, max, SeriesNumber } = mappedAnnotation;
let maxStr = '';
if (max) {
const roundedMax = utils.roundNumber(max, 2);
maxStr = `Max: ${roundedMax} <small>${getDisplayUnit(unit)}</small> `;
}
const maxStr = getStatisticDisplayString(max, unit, 'max');
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
if (!displayText.includes(str)) {

View File

@ -1,6 +1,5 @@
import SUPPORTED_TOOLS from './constants/supportedTools';
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
import { getDisplayUnit } from './utils';
import { utils } from '@ohif/core';
/**

View File

@ -0,0 +1,12 @@
import { utils } from '@ohif/core';
import getDisplayUnit from './getDisplayUnit';
export const getStatisticDisplayString = (numbers, unit, key) => {
if (Array.isArray(numbers) && numbers.length > 0) {
const results = numbers.map(number => utils.roundNumber(number, 2));
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${results.join(', ')} <small>${getDisplayUnit(unit)}</small>`;
}
const result = utils.roundNumber(numbers, 2);
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${result} <small>${getDisplayUnit(unit)}</small>`;
};

View File

@ -55,10 +55,13 @@ function createTools(utilityModule) {
parentTool: 'Brush',
configuration: {
activeStrategy: 'THRESHOLD_INSIDE_CIRCLE',
preview: {
enabled: true,
},
// preview: {
// enabled: true,
// },
strategySpecificConfiguration: {
// to use the use the center segment index to determine
// if inside -> same segment, if outside -> eraser
// useCenterSegmentIndex: true,
THRESHOLD: {
isDynamic: true,
dynamicRadius: 3,

View File

@ -112,21 +112,15 @@ const toolbarButtons: Button[] = [
commands: {
commandName: 'setBrushSize',
commandOptions: {
toolNames: ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
toolNames: [
'ThresholdCircularBrush',
'ThresholdSphereBrush',
'ThresholdCircularBrushDynamic',
],
},
},
},
{
name: 'Shape',
type: 'radio',
id: 'eraser-mode',
value: 'ThresholdCircularBrush',
values: [
{ value: 'ThresholdCircularBrush', label: 'Circle' },
{ value: 'ThresholdSphereBrush', label: 'Sphere' },
],
commands: 'setToolActiveToolbar',
},
{
name: 'Threshold',
type: 'radio',
@ -148,6 +142,19 @@ const toolbarButtons: Button[] = [
}
},
},
{
name: 'Shape',
type: 'radio',
id: 'eraser-mode',
value: 'ThresholdCircularBrush',
values: [
{ value: 'ThresholdCircularBrush', label: 'Circle' },
{ value: 'ThresholdSphereBrush', label: 'Sphere' },
],
condition: ({ options }) =>
options.find(option => option.id === 'dynamic-mode').value === 'ThresholdRange',
commands: 'setToolActiveToolbar',
},
{
name: 'ThresholdRange',
type: 'double-range',

View File

@ -1,7 +1,7 @@
import { hotkeys, classes } from '@ohif/core';
import toolbarButtons from './toolbarButtons.js';
import { id } from './id.js';
import initToolGroups, { toolGroupIds } from './initToolGroups.js';
import initToolGroups from './initToolGroups.js';
import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js';
import setFusionActiveVolume from './utils/setFusionActiveVolume.js';
import i18n from 'i18next';
@ -112,19 +112,6 @@ function modeFactory({ modeConfiguration }) {
},
]);
// This is a hack and we need to find a better way to enable
// some tools that require the viewport to be ready
const { unsubscribe: unsub1 } = viewportGridService.subscribe(
viewportGridService.EVENTS.VIEWPORTS_READY,
() => {
commandsManager.run('setToolEnabled', {
toolName: 'OrientationMarker',
toolGroupId: 'mipToolGroup',
});
unsub1();
}
);
// For the hanging protocol we need to decide on the window level
// based on whether the SUV is corrected or not, hence we can't hard
// code the window level in the hanging protocol but we add a custom

View File

@ -242,7 +242,7 @@ export default function ModeRoute({
const lowerVal = val.toLowerCase();
if (lowerVal !== 'studyinstanceuids') {
// Not sure why the case matters here - it doesn't in the URL
if (lowerVal === 'seriesinstanceuid') {
if (lowerVal === 'seriesinstanceuids') {
const seriesUIDs = getSplitParam(lowerVal, query);
return {
...acc,

View File

@ -4,7 +4,7 @@ import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
import { utils, Enums } from '@ohif/core';
const { sortingCriteria } = utils;
const { sortingCriteria, getSplitParam } = utils;
/**
* Initialize the route.
@ -95,6 +95,18 @@ export async function defaultRouteInit(
});
});
// is displaysets from URL and has initialSOPInstanceUID or initialSeriesInstanceUID
// then we need to wait for all display sets to be retrieved before applying the hanging protocol
const params = new URLSearchParams(window.location.search);
const initialSeriesInstanceUID = getSplitParam('initialseriesinstanceuid', params);
const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid', params);
let displaySetFromUrl = false;
if (initialSeriesInstanceUID || initialSOPInstanceUID) {
displaySetFromUrl = true;
}
await Promise.allSettled(allRetrieves).then(async promises => {
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
@ -109,7 +121,16 @@ export async function defaultRouteInit(
promises.forEach(promise => {
const retrieveSeriesMetadataPromise = promise.value;
if (Array.isArray(retrieveSeriesMetadataPromise)) {
if (!Array.isArray(retrieveSeriesMetadataPromise)) {
return;
}
if (displaySetFromUrl) {
const requiredSeriesPromises = retrieveSeriesMetadataPromise.map(promise =>
promise.start()
);
allPromises.push(Promise.allSettled(requiredSeriesPromises));
} else {
const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun(
hangingProtocolId,
retrieveSeriesMetadataPromise

View File

@ -1314,19 +1314,18 @@ export default class HangingProtocolService extends PubSubService {
);
// Use the display set provided instead
// Todo: find out what is wrong here
// if (reuseDisplaySetUID) {
// if (viewportOptions.allowUnmatchedView !== true) {
// this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
// }
// const displaySetInfo: HangingProtocol.DisplaySetInfo = {
// displaySetInstanceUID: reuseDisplaySetUID,
// displaySetOptions,
// };
if (reuseDisplaySetUID) {
if (viewportOptions.allowUnmatchedView !== true) {
this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
}
const displaySetInfo: HangingProtocol.DisplaySetInfo = {
displaySetInstanceUID: reuseDisplaySetUID,
displaySetOptions,
};
// displaySetsInfo.push(displaySetInfo);
// return;
// }
displaySetsInfo.push(displaySetInfo);
return;
}
// Use the display set index to allow getting the "next" match, eg
// matching all display sets, and get the matchedDisplaySetsIndex'th item

View File

@ -13,15 +13,16 @@ const isDisplaySetFromUrl = (displaySet): boolean => {
if (!initialSeriesInstanceUID && !initialSOPInstanceUID) {
return false;
}
const isSeriesMatch =
!initialSeriesInstanceUID ||
initialSeriesInstanceUID.some(seriesUID => displaySet.SeriesInstanceUID === seriesUID);
const isSopMatch =
!initialSOPInstanceUID ||
displaySet.instances?.some?.(instance =>
initialSOPInstanceUID.some(sopUID => sopUID === instance.SOPInstanceUID)
);
return isSeriesMatch && isSopMatch;
const isSeriesMatch = initialSeriesInstanceUID?.some(
seriesUID => displaySet.SeriesInstanceUID === seriesUID
);
const isSopMatch = initialSOPInstanceUID?.some(sopUID =>
displaySet.instances?.some(instance => sopUID === instance.SOPInstanceUID)
);
return isSeriesMatch || isSopMatch;
};
/** Returns the index location of the requested image, or the defaultValue in this.

View File

@ -28,7 +28,6 @@ export default function getPixelSpacingInformation(instance) {
PixelSpacingCalibrationType,
PixelSpacingCalibrationDescription,
EstimatedRadiographicMagnificationFactor,
SequenceOfUltrasoundRegions,
} = instance;
const isProjection = projectionRadiographSOPClassUIDs.includes(SOPClassUID);
@ -90,21 +89,6 @@ export default function getPixelSpacingInformation(instance) {
PixelSpacing: CorrectedImagerPixelSpacing,
isProjection,
};
} else if (SequenceOfUltrasoundRegions && typeof SequenceOfUltrasoundRegions === 'object') {
const { PhysicalDeltaX, PhysicalDeltaY } = SequenceOfUltrasoundRegions;
const USPixelSpacing = [PhysicalDeltaX * 10, PhysicalDeltaY * 10];
return {
PixelSpacing: USPixelSpacing,
};
} else if (
SequenceOfUltrasoundRegions &&
Array.isArray(SequenceOfUltrasoundRegions) &&
SequenceOfUltrasoundRegions.length > 1
) {
log.warn(
'Sequence of Ultrasound Regions > one entry. This is not yet implemented, all measurements will be shown in pixels.'
);
} else if (isProjection === false && !ImagerPixelSpacing) {
// If only Pixel Spacing is present, and this is not a projection radiograph,
// we can stop here

View File

@ -120,47 +120,74 @@ values.
:::
### SeriesInstanceUID, SeriesInstanceUIDs and initialSeriesInstanceUID
### SeriesInstanceUIDs
Sometimes you need to only retrieve a specific series in a study, you can do
that by providing series level QIDO query parameters in the URL such as
SeriesInstanceUID or SeriesNumber. This does NOT work with instance or study
SeriesInstanceUIDs. This does NOT work with instance or study
level parameters. For example:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8
```
This will only open the viewer with one series (one displaySet) loaded, and no
queries made for any other series.
Sometimes you need to only retrieve a subset of series in a study, you can do
that by providing study level wado query parameters in the URL such as
SeriesInstanceUIDs. For example:
that by providing more than one series, separated by commas. For example:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4,1.3.6.1.4.1.25403.345050719074.3824.20170125113545.5
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10
```
This will only open the viewer with two series (two displaySets) loaded, and no
queries made for any other series.
Alternatively, sometimes you want to just open the study on a specified series
and/or display a particular sop instance, which you can accomplish using:
`initialSeriesInstanceUID` and/or `initialSOPInstanceUID`
to select the series to open on, but allowing other
series to be present in the study browser panel. This is the same behaviour
as in OHIF 2.0, albeit on different URL parameters. For example:
### initialSeriesInstanceUID
Alternatively, sometimes you want to just open the study on a specified series, but allowing other
series to be present too. This is the same behavior can be
achieved by using the `initialSeriesInstanceUID` parameter. For example:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8
```
This will open all the series in the study, but the viewer will start with the
series specified by the `initialSeriesInstanceUID` parameter.
Note that you can combine these, if you want to load a specific set of series
plus show an initial one as the first one selected, for example:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113546.1
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10
```
### initialSopInstanceUID
You can also specify the initial SOP Instance to be displayed by using the
`initialSopInstanceUID` parameter. For example:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095501.9
```
This will open the study with the filtered series, and navigate to the slice 101
which happens to be the SOP Instance specified by the `initialSopInstanceUID`
Note: again you can mix and match
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&SeriesInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095449.8,1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095506.10&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8
```
You can even load the whole study and only specify the initial SOP Instance to be displayed. Although
it will take more time to match, but it works as expected.
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095510.8
```
### hangingProtocolId
@ -169,6 +196,11 @@ You can select the initial hanging protocol to apply by using the
hangingProtocolId parameter. The selected parameter must be available in a
hangingProtocolModule registration, but does not have to be active.
For instance for loading a specific study in mpr mode from start you can use:
```js
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingProtocolId=@ohif/mnGrid
```
### token