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:
Bill Wallace authored and GitHub committed 2023-04-05 12:59:56 -04:00
1 parent 226244a26c
commit 50ed96ff73
16 files changed
+284 -47

No files matched your search

+5 -1
View File
@@ -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 };
+15 -7
View File
@@ -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
+5
View File
@@ -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;
+2
View File
@@ -13,6 +13,8 @@ describe('Top level exports', () => {
'sortStudyInstances',
'sortStudySeries',
'sortingCriteria',
'splitComma',
'getSplitParam',
'isLowPriorityModality',
'writeScript',
'debounce',
+31
View 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 };