feat(URL): add param for initial series and sop uids to display (#3265)
* feat: Allow navigating to a specified series and sop instance This was a feature in OHIF v2, so adding it to v3, albeit with new parameters. feat: Allow comma separated as well as repeated args params * docs * Test fixes * feat: Navigate to SOP selected - PR fixes * Updated docs * PR fixes
This commit is contained in:
parent
226244a26c
commit
50ed96ff73
@ -22,8 +22,8 @@ import {
|
|||||||
retrieveStudyMetadata,
|
retrieveStudyMetadata,
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
} from './retrieveStudyMetadata.js';
|
} from './retrieveStudyMetadata.js';
|
||||||
import StaticWadoClient from './utils/StaticWadoClient.js';
|
import StaticWadoClient from './utils/StaticWadoClient';
|
||||||
import getDirectURL from '../utils/getDirectURL.js';
|
import getDirectURL from '../utils/getDirectURL';
|
||||||
|
|
||||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
|
||||||
@ -90,7 +90,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
|
|||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: ({ params, query }) => {
|
initialize: ({ params, query }) => {
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||||
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
const queryStudyInstanceUIDs = utils.splitComma(
|
||||||
|
query.getAll('StudyInstanceUIDs')
|
||||||
|
);
|
||||||
|
|
||||||
const StudyInstanceUIDs =
|
const StudyInstanceUIDs =
|
||||||
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
|
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
|
||||||
|
|||||||
@ -9,18 +9,19 @@ import { api } from 'dicomweb-client';
|
|||||||
*/
|
*/
|
||||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||||
static studyFilterKeys = {
|
static studyFilterKeys = {
|
||||||
StudyInstanceUID: '0020000D',
|
studyinstanceuid: '0020000D',
|
||||||
PatientName: '00100010',
|
patientname: '00100010',
|
||||||
'00100020': 'mrn',
|
'00100020': 'mrn',
|
||||||
StudyDescription: '00081030',
|
studydescription: '00081030',
|
||||||
StudyDate: '00080020',
|
studydate: '00080020',
|
||||||
ModalitiesInStudy: '00080061',
|
modalitiesinstudy: '00080061',
|
||||||
AccessionNumber: '00080050',
|
accessionnumber: '00080050',
|
||||||
};
|
};
|
||||||
|
|
||||||
static seriesFilterKeys = {
|
static seriesFilterKeys = {
|
||||||
SeriesInstanceUID: '0020000E',
|
seriesinstanceuid: '0020000E',
|
||||||
SeriesNumber: '00200011',
|
seriesnumber: '00200011',
|
||||||
|
modality: '00080060',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(qidoConfig) {
|
constructor(qidoConfig) {
|
||||||
@ -37,15 +38,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
async searchForStudies(options) {
|
async searchForStudies(options) {
|
||||||
if (!this.staticWado) return super.searchForStudies(options);
|
if (!this.staticWado) return super.searchForStudies(options);
|
||||||
|
|
||||||
let searchResult = await super.searchForStudies(options);
|
const searchResult = await super.searchForStudies(options);
|
||||||
const { queryParams } = options;
|
const { queryParams } = options;
|
||||||
|
|
||||||
if (!queryParams) return searchResult;
|
if (!queryParams) return searchResult;
|
||||||
|
|
||||||
|
const lowerParams = this.toLowerParams(queryParams);
|
||||||
const filtered = searchResult.filter(study => {
|
const filtered = searchResult.filter(study => {
|
||||||
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
||||||
if (
|
if (
|
||||||
!this.filterItem(
|
!this.filterItem(
|
||||||
key,
|
key,
|
||||||
queryParams,
|
lowerParams,
|
||||||
study,
|
study,
|
||||||
StaticWadoClient.studyFilterKeys
|
StaticWadoClient.studyFilterKeys
|
||||||
)
|
)
|
||||||
@ -61,16 +65,18 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
async searchForSeries(options) {
|
async searchForSeries(options) {
|
||||||
if (!this.staticWado) return super.searchForSeries(options);
|
if (!this.staticWado) return super.searchForSeries(options);
|
||||||
|
|
||||||
let searchResult = await super.searchForSeries(options);
|
const searchResult = await super.searchForSeries(options);
|
||||||
const { queryParams } = options;
|
const { queryParams } = options;
|
||||||
if (!queryParams) return searchResult;
|
if (!queryParams) return searchResult;
|
||||||
const filtered = searchResult.filter(study => {
|
const lowerParams = this.toLowerParams(queryParams);
|
||||||
|
|
||||||
|
const filtered = searchResult.filter(series => {
|
||||||
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
||||||
if (
|
if (
|
||||||
!this.filterItem(
|
!this.filterItem(
|
||||||
key,
|
key,
|
||||||
queryParams,
|
lowerParams,
|
||||||
study,
|
series,
|
||||||
StaticWadoClient.seriesFilterKeys
|
StaticWadoClient.seriesFilterKeys
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@ -136,13 +142,13 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
/**
|
/**
|
||||||
* Filters the return list by the query parameters.
|
* Filters the return list by the query parameters.
|
||||||
*
|
*
|
||||||
* @param {*} key
|
* @param anyCaseKey - a possible search key
|
||||||
* @param {*} queryParams
|
* @param queryParams -
|
||||||
* @param {*} study
|
* @param {*} study
|
||||||
* @param {*} sourceFilterMap
|
* @param {*} sourceFilterMap
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
filterItem(key, queryParams, study, sourceFilterMap) {
|
filterItem(key: string, queryParams, study, sourceFilterMap) {
|
||||||
const altKey = sourceFilterMap[key] || key;
|
const altKey = sourceFilterMap[key] || key;
|
||||||
if (!queryParams) return true;
|
if (!queryParams) return true;
|
||||||
const testValue = queryParams[key] || queryParams[altKey];
|
const testValue = queryParams[key] || queryParams[altKey];
|
||||||
@ -153,6 +159,15 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
return this.compareDateRange(testValue, valueElem.Value[0]);
|
return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||||
}
|
}
|
||||||
const value = valueElem.Value;
|
const value = valueElem.Value;
|
||||||
return this.compareValues(testValue, value) && true;
|
return this.compareValues(testValue, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts the query parameters to lower case query parameters */
|
||||||
|
toLowerParams(queryParams: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const lowerParams = {};
|
||||||
|
Object.entries(queryParams).forEach(([key, value]) => {
|
||||||
|
lowerParams[key.toLowerCase()] = value;
|
||||||
|
});
|
||||||
|
return lowerParams;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,7 +7,7 @@ const defaultProtocol = {
|
|||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
createdDate: '2021-02-23T19:22:08.894Z',
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
modifiedDate: '2021-02-23T19:22:08.894Z',
|
modifiedDate: '2023-04-01',
|
||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
@ -33,8 +33,6 @@ const defaultProtocol = {
|
|||||||
},
|
},
|
||||||
displaySetSelectors: {
|
displaySetSelectors: {
|
||||||
defaultDisplaySetId: {
|
defaultDisplaySetId: {
|
||||||
// Unused currently
|
|
||||||
imageMatchingRules: [],
|
|
||||||
// Matches displaysets, NOT series
|
// Matches displaysets, NOT series
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: [
|
||||||
// Try to match series with images by default, to prevent weird display
|
// Try to match series with images by default, to prevent weird display
|
||||||
@ -45,6 +43,15 @@ const defaultProtocol = {
|
|||||||
greaterThan: { value: 0 },
|
greaterThan: { value: 0 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// This display set will select the specified items by preference
|
||||||
|
// It has no affect if nothing is specified in the URL.
|
||||||
|
{
|
||||||
|
attribute: 'isDisplaySetFromUrl',
|
||||||
|
weight: 10,
|
||||||
|
constraint: {
|
||||||
|
equals: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
// Can be used to select matching studies
|
// Can be used to select matching studies
|
||||||
// studyMatchingRules: [],
|
// studyMatchingRules: [],
|
||||||
@ -65,7 +72,13 @@ const defaultProtocol = {
|
|||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
viewportType: 'stack',
|
viewportType: 'stack',
|
||||||
toolGroupId: 'default',
|
toolGroupId: 'default',
|
||||||
// initialImageOptions: {
|
// This will specify the initial image options index if it matches in the URL
|
||||||
|
// and will otherwise not specify anything.
|
||||||
|
initialImageOptions: {
|
||||||
|
custom: 'sopInstanceLocation',
|
||||||
|
},
|
||||||
|
// Other options for initialImageOptions, which can be included in the default
|
||||||
|
// custom attribute, or can be provided directly.
|
||||||
// index: 180,
|
// index: 180,
|
||||||
// preset: 'middle', // 'first', 'last', 'middle'
|
// preset: 'middle', // 'first', 'last', 'middle'
|
||||||
// },
|
// },
|
||||||
|
|||||||
@ -133,7 +133,6 @@ function modeFactory() {
|
|||||||
const {
|
const {
|
||||||
toolGroupService,
|
toolGroupService,
|
||||||
syncGroupService,
|
syncGroupService,
|
||||||
toolbarService,
|
|
||||||
segmentationService,
|
segmentationService,
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|||||||
@ -30,6 +30,10 @@ class ImageSet {
|
|||||||
writable: false,
|
writable: false,
|
||||||
value: guid(), // Unique ID of the instance
|
value: guid(), // Unique ID of the instance
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.instances = images;
|
||||||
|
this.instance = images[0];
|
||||||
|
this.StudyInstanceUID = this.instance?.StudyInstanceUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUID() {
|
getUID() {
|
||||||
@ -56,7 +60,7 @@ class ImageSet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNumImages = () => this.images.length
|
getNumImages = () => this.images.length;
|
||||||
|
|
||||||
getImage(index) {
|
getImage(index) {
|
||||||
return this.images[index];
|
return this.images[index];
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
|||||||
import { CommandsManager } from '../../classes';
|
import { CommandsManager } from '../../classes';
|
||||||
import ServicesManager from '../ServicesManager';
|
import ServicesManager from '../ServicesManager';
|
||||||
import * as HangingProtocol from '../../types/HangingProtocol';
|
import * as HangingProtocol from '../../types/HangingProtocol';
|
||||||
|
import {
|
||||||
|
isDisplaySetFromUrl,
|
||||||
|
sopInstanceLocation,
|
||||||
|
} from './isDisplaySetFromUrl';
|
||||||
|
|
||||||
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
|
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
|
||||||
|
|
||||||
@ -81,6 +85,14 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
// we can add more advanced checking here
|
// we can add more advanced checking here
|
||||||
callback: displaySet => displaySet.isReconstructable ?? false,
|
callback: displaySet => displaySet.isReconstructable ?? false,
|
||||||
},
|
},
|
||||||
|
isDisplaySetFromUrl: {
|
||||||
|
name: 'Checks if the display set is as specified in the URL',
|
||||||
|
callback: isDisplaySetFromUrl,
|
||||||
|
},
|
||||||
|
sopInstanceLocation: {
|
||||||
|
name: 'Gets the position of the specified sop instance',
|
||||||
|
callback: sopInstanceLocation,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
listeners = {};
|
listeners = {};
|
||||||
registeredImageLoadStrategies = {};
|
registeredImageLoadStrategies = {};
|
||||||
@ -648,6 +660,39 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
return viewportsToUpdate;
|
return viewportsToUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a computed options value, or a copy of the options
|
||||||
|
* This allows computing values such as the initial image index to use
|
||||||
|
* based on custom attribute functions, the same as the validators.
|
||||||
|
* Computing individual values is something that can be declared statically
|
||||||
|
* as long as the named functions are provided ahead of time, which is much
|
||||||
|
* simpler than recomputing the entire protocol.
|
||||||
|
*/
|
||||||
|
public getComputedOptions(
|
||||||
|
options: Record<string, unknown>,
|
||||||
|
displaySetUIDs: string[]
|
||||||
|
) {
|
||||||
|
const computed = { ...options };
|
||||||
|
let displaySets;
|
||||||
|
for (const key in computed) {
|
||||||
|
const value = computed[key];
|
||||||
|
if (!value) continue;
|
||||||
|
if (value.custom) {
|
||||||
|
if (!displaySets) {
|
||||||
|
displaySets = this.displaySets.filter(
|
||||||
|
displaySet =>
|
||||||
|
displaySetUIDs.indexOf(displaySet.displaySetInstanceUID) !== -1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
computed[key] = this.customAttributeRetrievalCallbacks[
|
||||||
|
value.custom
|
||||||
|
].callback.call(computed, displaySets);
|
||||||
|
if (computed[key] === undefined) computed[key] = computed.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It applied the protocol to the current studies and display sets based on the
|
* It applied the protocol to the current studies and display sets based on the
|
||||||
* protocolId that is provided.
|
* protocolId that is provided.
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { getSplitParam } from '../../utils';
|
||||||
|
|
||||||
|
/** Indicates if the given display set is the one specified in the
|
||||||
|
* displaySet parameter in the URL
|
||||||
|
* The parameters are:
|
||||||
|
* initialSeriesInstanceUID
|
||||||
|
* initialSOPInstanceUID
|
||||||
|
*/
|
||||||
|
const isDisplaySetFromUrl = (displaySet): boolean => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const initialSeriesInstanceUID = getSplitParam(
|
||||||
|
'initialseriesinstanceuid',
|
||||||
|
params
|
||||||
|
);
|
||||||
|
const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid', params);
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns the index location of the requested image, or the defaultValue in this.
|
||||||
|
* Returns undefined to fallback to the defaultValue
|
||||||
|
*/
|
||||||
|
function sopInstanceLocation(displaySets) {
|
||||||
|
const displaySet = displaySets[0];
|
||||||
|
if (!displaySet) return;
|
||||||
|
const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid');
|
||||||
|
if (!initialSOPInstanceUID) return;
|
||||||
|
|
||||||
|
const index = displaySet.instances.findIndex(instance =>
|
||||||
|
initialSOPInstanceUID.includes(instance.SOPInstanceUID)
|
||||||
|
);
|
||||||
|
// Need to return in the initial position specified format.
|
||||||
|
return index === -1 ? undefined : { index };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isDisplaySetFromUrl, sopInstanceLocation };
|
||||||
@ -135,19 +135,27 @@ export type SyncGroup = {
|
|||||||
target?: boolean;
|
target?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Declares a custom option, that is a computed type value */
|
||||||
|
export type CustomOptionAttribute<T> = {
|
||||||
|
custom: string;
|
||||||
|
defaultValue?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomOption<T> = CustomOptionAttribute<T> | T;
|
||||||
|
|
||||||
export type initialImageOptions = {
|
export type initialImageOptions = {
|
||||||
index?: number;
|
index?: number;
|
||||||
preset?: string; // todo: type more
|
preset?: string; // todo: type more
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ViewportOptions = {
|
export type ViewportOptions = {
|
||||||
toolGroupId?: string;
|
toolGroupId?: CustomOption<string>;
|
||||||
viewportType?: string;
|
viewportType?: CustomOption<string>;
|
||||||
id?: string;
|
id?: string;
|
||||||
orientation?: string;
|
orientation?: CustomOption<string>;
|
||||||
viewportId?: string;
|
viewportId?: string;
|
||||||
initialImageOptions?: initialImageOptions;
|
initialImageOptions?: CustomOption<initialImageOptions>;
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: CustomOption<SyncGroup>[];
|
||||||
customViewportProps?: Record<string, unknown>;
|
customViewportProps?: Record<string, unknown>;
|
||||||
// Set to true to allow non-matching drag and drop or options provided
|
// Set to true to allow non-matching drag and drop or options provided
|
||||||
// from options.displaySetSelectorsMap
|
// from options.displaySetSelectorsMap
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
seriesSortCriteria,
|
seriesSortCriteria,
|
||||||
} from './sortStudy';
|
} from './sortStudy';
|
||||||
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
|
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
|
||||||
|
import { splitComma, getSplitParam } from './splitComma';
|
||||||
|
|
||||||
// Commented out unused functionality.
|
// Commented out unused functionality.
|
||||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||||
@ -71,6 +72,8 @@ const utils = {
|
|||||||
roundNumber,
|
roundNumber,
|
||||||
downloadCSVReport,
|
downloadCSVReport,
|
||||||
subscribeToNextViewportGridChange,
|
subscribeToNextViewportGridChange,
|
||||||
|
splitComma,
|
||||||
|
getSplitParam,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -100,6 +103,8 @@ export {
|
|||||||
debounce,
|
debounce,
|
||||||
roundNumber,
|
roundNumber,
|
||||||
downloadCSVReport,
|
downloadCSVReport,
|
||||||
|
splitComma,
|
||||||
|
getSplitParam,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
@ -13,6 +13,8 @@ describe('Top level exports', () => {
|
|||||||
'sortStudyInstances',
|
'sortStudyInstances',
|
||||||
'sortStudySeries',
|
'sortStudySeries',
|
||||||
'sortingCriteria',
|
'sortingCriteria',
|
||||||
|
'splitComma',
|
||||||
|
'getSplitParam',
|
||||||
'isLowPriorityModality',
|
'isLowPriorityModality',
|
||||||
'writeScript',
|
'writeScript',
|
||||||
'debounce',
|
'debounce',
|
||||||
|
|||||||
31
platform/core/src/utils/splitComma.ts
Normal file
31
platform/core/src/utils/splitComma.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/** Splits a list of stirngs by commas within the strings */
|
||||||
|
const splitComma = (strings: string[]): string[] => {
|
||||||
|
if (!strings) return null;
|
||||||
|
for (let i = 0; i < strings.length; i++) {
|
||||||
|
const comma = strings[i].indexOf(',');
|
||||||
|
if (comma !== -1) {
|
||||||
|
const splits = strings[i].split(/,/);
|
||||||
|
strings.splice(i, 1, ...splits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of the comma split parameters from the given URL search params
|
||||||
|
* @param lowerCaseKey - lower case search parameter value
|
||||||
|
* @param params - URLSearchParams
|
||||||
|
* @returns Array of comma split items matching, or null
|
||||||
|
*/
|
||||||
|
const getSplitParam = (
|
||||||
|
lowerCaseKey: string,
|
||||||
|
params = new URLSearchParams(window.location.search)
|
||||||
|
): string[] => {
|
||||||
|
return splitComma(
|
||||||
|
[...params]
|
||||||
|
.find(([key, value]) => key.toLowerCase() === lowerCaseKey && value)
|
||||||
|
?.slice?.(1)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { splitComma, getSplitParam };
|
||||||
@ -110,17 +110,46 @@ You can open more than one study in the Viewer by adding the `StudyInstanceUIDs`
|
|||||||
:::tip
|
:::tip
|
||||||
|
|
||||||
You can ues this feature to open a current and prior study in the Viewer.
|
You can ues this feature to open a current and prior study in the Viewer.
|
||||||
Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section
|
Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section. You can also use commas to separate
|
||||||
|
values.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
### SeriesInstanceUIDs
|
### SeriesInstanceUID and initialSeriesInstanceUID
|
||||||
|
|
||||||
Sometimes you need to only open a specific series in a study, you can do that by
|
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
|
||||||
|
level parameters. For example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095748.1
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only open the viewer with one series (one displaySet).
|
This will only open the viewer with one series (one displaySet) 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:
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### hangingProtocolId
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@ -218,7 +218,8 @@ the display set is the same as the other viewports, but the
|
|||||||
from the display set selector which isn't already filling a view.
|
from the display set selector which isn't already filling a view.
|
||||||
|
|
||||||
## Custom Attribute
|
## Custom Attribute
|
||||||
In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance,
|
In some situations, you might want to match based on a custom
|
||||||
|
attribute and not the DICOM tags. For instance,
|
||||||
if you have assigned a `timepointId` to each study, and you want to match based on it.
|
if you have assigned a `timepointId` to each study, and you want to match based on it.
|
||||||
Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching.
|
Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching.
|
||||||
|
|
||||||
@ -294,3 +295,18 @@ function modeFactory() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Attributes for Viewport Options
|
||||||
|
|
||||||
|
The custom attributes can also be used for viewport options. This example,
|
||||||
|
from the default hanging protocol navigates the image to the image
|
||||||
|
specified in the URL:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
viewportOptions: {
|
||||||
|
initialImageOptions: {
|
||||||
|
// custom attribute name is selected by 'custom'
|
||||||
|
custom: 'sopInstanceLocation',
|
||||||
|
// This is the value returned if the above doesn't return anything
|
||||||
|
defaultValue: { index: 5 },
|
||||||
|
```
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
describe('OHIF HP', () => {
|
describe('OHIF HP', () => {
|
||||||
beforeEach(() => {
|
const beforeSetup = () => {
|
||||||
cy.checkStudyRouteInViewer(
|
cy.checkStudyRouteInViewer(
|
||||||
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
|
'1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1',
|
||||||
'&hangingProtocolId=@ohif/hp-extension.mn'
|
'&hangingProtocolId=@ohif/hp-extension.mn'
|
||||||
@ -7,15 +7,19 @@ describe('OHIF HP', () => {
|
|||||||
cy.expectMinimumThumbnails(3);
|
cy.expectMinimumThumbnails(3);
|
||||||
cy.initCornerstoneToolsAliases();
|
cy.initCornerstoneToolsAliases();
|
||||||
cy.initCommonElementsAliases();
|
cy.initCommonElementsAliases();
|
||||||
});
|
};
|
||||||
|
|
||||||
it('Should display 3 up', () => {
|
it('Should display 3 up', () => {
|
||||||
|
beforeSetup();
|
||||||
|
|
||||||
cy.get('[data-cy="viewport-pane"]')
|
cy.get('[data-cy="viewport-pane"]')
|
||||||
.its('length')
|
.its('length')
|
||||||
.should('be.eq', 3);
|
.should('be.eq', 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should navigate next/previous stage', () => {
|
it('Should navigate next/previous stage', () => {
|
||||||
|
beforeSetup();
|
||||||
|
|
||||||
cy.get('body').type(',');
|
cy.get('body').type(',');
|
||||||
cy.wait(250);
|
cy.wait(250);
|
||||||
cy.get('[data-cy="viewport-pane"]')
|
cy.get('[data-cy="viewport-pane"]')
|
||||||
@ -28,4 +32,19 @@ describe('OHIF HP', () => {
|
|||||||
.its('length')
|
.its('length')
|
||||||
.should('be.eq', 2);
|
.should('be.eq', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should navigate to display set specified', () => {
|
||||||
|
// This filters by series instance UID, meaning there will only be 1 thumbnail
|
||||||
|
// It applies the initial SOP instance, navigating to that image
|
||||||
|
cy.checkStudyRouteInViewer(
|
||||||
|
'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'
|
||||||
|
);
|
||||||
|
cy.expectMinimumThumbnails(1);
|
||||||
|
cy.initCornerstoneToolsAliases();
|
||||||
|
cy.initCommonElementsAliases();
|
||||||
|
|
||||||
|
// The specified series/sop UID's are index 101, so ensure that image is displayed
|
||||||
|
cy.get('@viewportInfoTopRight').should('contains.text', 'I:6');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -99,9 +99,10 @@ function ViewerViewportGrid(props) {
|
|||||||
return {
|
return {
|
||||||
displaySetInstanceUIDs: displaySetUIDsToHang,
|
displaySetInstanceUIDs: displaySetUIDsToHang,
|
||||||
displaySetOptions: displaySetUIDsToHangOptions,
|
displaySetOptions: displaySetUIDsToHangOptions,
|
||||||
viewportOptions: {
|
viewportOptions: hangingProtocolService.getComputedOptions(
|
||||||
...viewportOptions,
|
viewportOptions,
|
||||||
},
|
displaySetUIDsToHang
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -279,8 +279,10 @@ export default function ModeRoute({
|
|||||||
const filters =
|
const filters =
|
||||||
Array.from(query.keys()).reduce(
|
Array.from(query.keys()).reduce(
|
||||||
(acc: Record<string, string>, val: string) => {
|
(acc: Record<string, string>, val: string) => {
|
||||||
if (val !== 'StudyInstanceUIDs') {
|
const lowerVal = val.toLowerCase();
|
||||||
if (['seriesInstanceUID', 'SeriesInstanceUID'].includes(val)) {
|
if (lowerVal !== 'studyinstanceuids') {
|
||||||
|
// Not sure why the case matters here - it doesn't in the URL
|
||||||
|
if (lowerVal === 'seriesinstanceuid') {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
seriesInstanceUID: query.get(val),
|
seriesInstanceUID: query.get(val),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user