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:
1 parent
226244a26c
commit
50ed96ff73
16 files changed
+284
-47
No files matched your search
@@ -30,6 +30,10 @@ class ImageSet {
|
||||
writable: false,
|
||||
value: guid(), // Unique ID of the instance
|
||||
});
|
||||
|
||||
this.instances = images;
|
||||
this.instance = images[0];
|
||||
this.StudyInstanceUID = this.instance?.StudyInstanceUID;
|
||||
}
|
||||
|
||||
getUID() {
|
||||
@@ -56,7 +60,7 @@ class ImageSet {
|
||||
}
|
||||
}
|
||||
|
||||
getNumImages = () => this.images.length
|
||||
getNumImages = () => this.images.length;
|
||||
|
||||
getImage(index) {
|
||||
return this.images[index];
|
||||
|
||||
@@ -6,6 +6,10 @@ import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
||||
import { CommandsManager } from '../../classes';
|
||||
import ServicesManager from '../ServicesManager';
|
||||
import * as HangingProtocol from '../../types/HangingProtocol';
|
||||
import {
|
||||
isDisplaySetFromUrl,
|
||||
sopInstanceLocation,
|
||||
} from './isDisplaySetFromUrl';
|
||||
|
||||
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
|
||||
|
||||
@@ -81,6 +85,14 @@ export default class HangingProtocolService extends PubSubService {
|
||||
// we can add more advanced checking here
|
||||
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 = {};
|
||||
registeredImageLoadStrategies = {};
|
||||
@@ -648,6 +660,39 @@ export default class HangingProtocolService extends PubSubService {
|
||||
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
|
||||
* 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 };
|
||||
@@ -70,8 +70,8 @@ export type ConstraintValue =
|
||||
| boolean
|
||||
| []
|
||||
| {
|
||||
value: string | number | boolean | [];
|
||||
};
|
||||
value: string | number | boolean | [];
|
||||
};
|
||||
|
||||
export type Constraint = {
|
||||
// This value exactly
|
||||
@@ -135,19 +135,27 @@ export type SyncGroup = {
|
||||
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 = {
|
||||
index?: number;
|
||||
preset?: string; // todo: type more
|
||||
};
|
||||
|
||||
export type ViewportOptions = {
|
||||
toolGroupId?: string;
|
||||
viewportType?: string;
|
||||
toolGroupId?: CustomOption<string>;
|
||||
viewportType?: CustomOption<string>;
|
||||
id?: string;
|
||||
orientation?: string;
|
||||
orientation?: CustomOption<string>;
|
||||
viewportId?: string;
|
||||
initialImageOptions?: initialImageOptions;
|
||||
syncGroups?: SyncGroup[];
|
||||
initialImageOptions?: CustomOption<initialImageOptions>;
|
||||
syncGroups?: CustomOption<SyncGroup>[];
|
||||
customViewportProps?: Record<string, unknown>;
|
||||
// Set to true to allow non-matching drag and drop or options provided
|
||||
// from options.displaySetSelectorsMap
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
seriesSortCriteria,
|
||||
} from './sortStudy';
|
||||
import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange';
|
||||
import { splitComma, getSplitParam } from './splitComma';
|
||||
|
||||
// Commented out unused functionality.
|
||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||
@@ -71,6 +72,8 @@ const utils = {
|
||||
roundNumber,
|
||||
downloadCSVReport,
|
||||
subscribeToNextViewportGridChange,
|
||||
splitComma,
|
||||
getSplitParam,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -100,6 +103,8 @@ export {
|
||||
debounce,
|
||||
roundNumber,
|
||||
downloadCSVReport,
|
||||
splitComma,
|
||||
getSplitParam,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -13,6 +13,8 @@ describe('Top level exports', () => {
|
||||
'sortStudyInstances',
|
||||
'sortStudySeries',
|
||||
'sortingCriteria',
|
||||
'splitComma',
|
||||
'getSplitParam',
|
||||
'isLowPriorityModality',
|
||||
'writeScript',
|
||||
'debounce',
|
||||
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user