feat: Start using group filtering to define measurements table layout (#4501)

This commit is contained in:
Bill Wallace authored and GitHub committed 2025-01-09 22:01:42 -05:00
1 parent bc73de9710
commit 82440e88d5
16 files changed
+357 -210

No files matched your search

+11 -1
View File
@@ -161,6 +161,16 @@ export class CommandsManager {
* Run one or more commands with specified extra options.
* Returns the result of the last command run.
*
* Example commands to run are:
* * 'updateMeasurement'
* * `{commandName: 'displayWhatever'}`
* * `['updateMeasurement', {commandName: 'displayWhatever'}]`
* * `{ commands: 'updateMeasurement' }`
* * `{ commands: ['updateMeasurement', {commandName: 'displayWhatever'}]}`
*
* Note how the various styles can be mixed, simplifying the declaration of
* sets of commands.
*
* @param toRun - A specification of one or more commands,
* typically an object of { commandName, commandOptions, context }
* or an array of such objects. It can also be a single commandName as string
@@ -169,7 +179,7 @@ export class CommandsManager {
* the commandOptions specified in the base.
*/
public run(
toRun: Command | Commands | Command[] | string | undefined,
toRun: Command | Commands | (Command | string)[] | string | undefined,
options?: Record<string, unknown>
): unknown {
if (!toRun) {
@@ -597,7 +597,10 @@ export default class ExtensionManager extends PubSubService {
}
Object.keys(definitions).forEach(commandName => {
const commandDefinition = definitions[commandName];
let commandDefinition = definitions[commandName];
if (typeof commandDefinition === 'function') {
commandDefinition = { commandFn: commandDefinition };
}
const commandHasContextThatDoesNotExist =
commandDefinition.context && !this._commandsManager.getContext(commandDefinition.context);
@@ -92,6 +92,8 @@ const VALUE_TYPES = {
ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual',
};
export type MeasurementFilter = (measurement) => boolean;
/**
* MeasurementService class that supports source management and measurement management.
* Sources can be any library that can provide "annotations" (e.g. cornerstone-tools, cornerstone, etc.)
@@ -108,7 +110,7 @@ class MeasurementService extends PubSubService {
public static REGISTRATION = {
name: 'measurementService',
altName: 'MeasurementService',
create: ({ configuration = {} }) => {
create: _options => {
return new MeasurementService();
},
};
@@ -120,10 +122,11 @@ class MeasurementService extends PubSubService {
private measurements = new Map();
private unmappedMeasurements = new Map();
private sources = {};
private mappings = {};
constructor() {
super(EVENTS);
this.sources = {};
this.mappings = {};
}
/**
@@ -164,12 +167,16 @@ class MeasurementService extends PubSubService {
}
/**
* Get all measurements.
* Gets measurements, optionally filtered by the filter
* function.
*
* @return {Measurement[]} Array of measurements
*/
getMeasurements() {
return [...this.measurements.values()];
public getMeasurements(filter?: MeasurementFilter) {
const measurements = [...this.measurements.values()];
return filter
? measurements.filter(measurement => filter.call(this, measurement))
: measurements;
}
/**
@@ -582,11 +589,20 @@ class MeasurementService extends PubSubService {
});
}
clearMeasurements() {
/**
* Clears measurements that match the filter, defaulting to all of them.
* That allows, for example, clearing all of a single studies measurements
* without needing to clear other measurements.
*/
public clearMeasurements(filter?: MeasurementFilter) {
// Make a copy of the measurements
const measurements = [...this.measurements.values(), ...this.unmappedMeasurements.values()];
this.unmappedMeasurements.clear();
this.measurements.clear();
const toClear = this.getMeasurements(filter);
const unmappedClear = filter
? [...this.unmappedMeasurements.values()].filter(filter)
: this.unmappedMeasurements;
const measurements = [...toClear, ...unmappedClear];
unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid));
toClear.forEach(measurement => this.measurements.delete(measurement.uid));
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
}
+1
View File
@@ -43,6 +43,7 @@ describe('Top level exports', () => {
'progressTrackingUtils',
'uuidv4',
'addAccessors',
'MeasurementFilters',
].sort();
const exports = Object.keys(utils.default).sort();
+3
View File
@@ -39,6 +39,7 @@ import {
import { splitComma, getSplitParam } from './splitComma';
import { createStudyBrowserTabs } from './createStudyBrowserTabs';
import { sopClassDictionary } from './sopClassDictionary';
import * as MeasurementFilters from './measurementFilters';
// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
@@ -84,6 +85,7 @@ const utils = {
getSplitParam,
generateAcceptHeader,
createStudyBrowserTabs,
MeasurementFilters,
};
export {
@@ -117,6 +119,7 @@ export {
getSplitParam,
generateAcceptHeader,
createStudyBrowserTabs,
MeasurementFilters,
};
export default utils;
@@ -0,0 +1,110 @@
/**
* Returns a filter function which filters for measurements belonging to both
* the study and series.
*/
export function filterMeasurementsBySeriesUID(selectedSeries: string[]) {
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
}
/**
* @returns true for measurements include referencedImageId (coplanar with an image)
*/
export function filterPlanarMeasurement(measurement) {
return measurement?.referencedImageId;
}
/** A filter that always returns true */
export function filterAny(_measurement) {
return true;
}
/** A filter that excludes everything */
export function filterNone(_measurement) {
return false;
}
/**
* Filters the measurements which are found in any of the specified
* filters. Strings will be looked up by name.
*/
export function filterOr(...filters) {
return function (item) {
for (let filter of filters) {
if (typeof filter === 'string') {
filter = this[filter];
}
if (typeof filter !== 'function') {
continue;
}
if (filter.call(this, item)) {
return true;
}
}
return false;
};
}
/**
* Filters for additional findings, that is, measurements with
* a value of type point, and having a referenced image
*/
export function filterAdditionalFindings(measurementService) {
const { POINT } = measurementService.VALUE_TYPES;
return dm => dm.type === POINT && dm.referencedImageId;
}
/**
* Returns a filter that applies the second filter unless the first filter would
* include the given measurement.
* That is, (!filterUnless) && filterThen
*/
export function filterUnless(filterUnless, filterThen) {
return item => (filterUnless(item) ? false : filterThen(item));
}
const isString = s => typeof s === 'string' || s instanceof String;
/**
* Returns true if all the filters return true.
* Any filter can be a string name of a filter on the "this" object
* called on the final filter call.
*/
export function filterAnd(...filters) {
return function (item) {
for (const filter of filters) {
if (isString(filter)) {
if (!this[filter](item)) {
return false;
}
} else if (!filter.call(this, item)) {
return false;
}
}
return true;
};
}
/**
* Returns a filter that returns true if none of the filters supplied return true.
* Any filter supplied can be a name, in which case hte filter will be retrieved
* from "this" object on the call.
*
* For example, for filterNot("otherFilterName"), if that is called on
* `{ otherFilterName: filterNone }`
* then otherFilterName will be called, returning false in this case and
* filterNot will return true.
*
*
*/
export function filterNot(...filters) {
if (filters.length !== 1) {
return filterAnd.apply(null, filters.map(filterNot));
}
const [filter] = filters;
if (isString(filter)) {
return function (item) {
return !this[filter](item);
};
}
return item => !filter(item);
}