init
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { roundNumber } from '@ohif/core/src/utils';
|
||||
import {
|
||||
SegmentationData,
|
||||
SegmentationRepresentation,
|
||||
} from '../services/SegmentationService/SegmentationService';
|
||||
|
||||
const excludedModalities = ['SM', 'OT', 'DOC', 'ECG'];
|
||||
|
||||
function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
const { label, segments } = segmentation;
|
||||
|
||||
// Get the readable text mapping once
|
||||
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||
'PanelSegmentation.readableText',
|
||||
{}
|
||||
);
|
||||
|
||||
// Helper function to recursively map cachedStats to readable display text
|
||||
function mapStatsToDisplay(stats, indent = 0) {
|
||||
const primary = [];
|
||||
const indentation = ' '.repeat(indent);
|
||||
|
||||
for (const key in stats) {
|
||||
if (Object.prototype.hasOwnProperty.call(stats, key)) {
|
||||
const value = stats[key];
|
||||
const readableText = readableTextMap?.[key];
|
||||
|
||||
if (!readableText) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Add empty row before category (except for the first category)
|
||||
if (primary.length > 0) {
|
||||
primary.push('');
|
||||
}
|
||||
// Add category title
|
||||
primary.push(`${indentation}${readableText}`);
|
||||
// Recursively handle nested objects
|
||||
primary.push(...mapStatsToDisplay(value, indent + 1));
|
||||
} else {
|
||||
// For non-nested values, don't add empty rows
|
||||
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return primary;
|
||||
}
|
||||
|
||||
// Get customization for display text mapping
|
||||
const displayTextMapper = segment => {
|
||||
const defaultDisplay = {
|
||||
primary: [],
|
||||
secondary: [],
|
||||
};
|
||||
|
||||
// If the segment has cachedStats, map it to readable text
|
||||
if (segment.cachedStats) {
|
||||
const primary = mapStatsToDisplay(segment.cachedStats);
|
||||
defaultDisplay.primary = primary;
|
||||
}
|
||||
|
||||
return defaultDisplay;
|
||||
};
|
||||
|
||||
const updatedSegments = {};
|
||||
|
||||
Object.entries(segments).forEach(([segmentIndex, segment]) => {
|
||||
updatedSegments[segmentIndex] = {
|
||||
...segment,
|
||||
displayText: displayTextMapper(segment),
|
||||
};
|
||||
});
|
||||
|
||||
// Map the segments and apply the display text mapper
|
||||
return {
|
||||
...segmentation,
|
||||
label,
|
||||
segments: updatedSegments,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the combination of segmentation data and its representation in a viewport.
|
||||
*/
|
||||
type ViewportSegmentationRepresentation = {
|
||||
segmentationsWithRepresentations: {
|
||||
representation: SegmentationRepresentation;
|
||||
segmentation: SegmentationData;
|
||||
}[];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook that provides segmentation data and their representations for the active viewport.
|
||||
* @param options - The options object.
|
||||
* @param options.servicesManager - The services manager object.
|
||||
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
|
||||
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||
* @returns An array of segmentation data and their representations for the active viewport.
|
||||
*/
|
||||
export function useActiveViewportSegmentationRepresentations({
|
||||
servicesManager,
|
||||
subscribeToDataModified = false,
|
||||
debounceTime = 0,
|
||||
}: withAppTypes<{ debounceTime?: number }>): ViewportSegmentationRepresentation {
|
||||
const { segmentationService, viewportGridService, customizationService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const [segmentationsWithRepresentations, setSegmentationsWithRepresentations] =
|
||||
useState<ViewportSegmentationRepresentation>({
|
||||
segmentationsWithRepresentations: [],
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const viewportId = viewportGridService.getActiveViewportId();
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewportId);
|
||||
|
||||
if (!displaySetUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUIDs[0]);
|
||||
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excludedModalities.includes(displaySet.Modality)) {
|
||||
setSegmentationsWithRepresentations(prev => ({
|
||||
segmentationsWithRepresentations: [],
|
||||
disabled: true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
if (!segmentations?.length) {
|
||||
setSegmentationsWithRepresentations(prev => ({
|
||||
segmentationsWithRepresentations: [],
|
||||
disabled: false,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const representations = segmentationService.getSegmentationRepresentations(viewportId);
|
||||
|
||||
const newSegmentationsWithRepresentations = representations.map(representation => {
|
||||
const segmentation = segmentationService.getSegmentation(representation.segmentationId);
|
||||
const mappedSegmentation = mapSegmentationToDisplay(segmentation, customizationService);
|
||||
return {
|
||||
representation,
|
||||
segmentation: mappedSegmentation,
|
||||
};
|
||||
});
|
||||
|
||||
setSegmentationsWithRepresentations({
|
||||
segmentationsWithRepresentations: newSegmentationsWithRepresentations,
|
||||
disabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
const debouncedUpdate =
|
||||
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
|
||||
|
||||
update();
|
||||
|
||||
const subscriptions = [
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
debouncedUpdate
|
||||
),
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
debouncedUpdate
|
||||
),
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED,
|
||||
debouncedUpdate
|
||||
),
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
debouncedUpdate
|
||||
),
|
||||
viewportGridService.subscribe(viewportGridService.EVENTS.GRID_STATE_CHANGED, debouncedUpdate),
|
||||
];
|
||||
|
||||
if (subscribeToDataModified) {
|
||||
subscriptions.push(
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
debouncedUpdate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
if (debounceTime > 0) {
|
||||
debouncedUpdate.cancel();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
customizationService,
|
||||
displaySetService,
|
||||
debounceTime,
|
||||
subscribeToDataModified,
|
||||
]);
|
||||
|
||||
return segmentationsWithRepresentations;
|
||||
}
|
||||
103
extensions/cornerstone/src/hooks/useMeasurements.ts
Normal file
103
extensions/cornerstone/src/hooks/useMeasurements.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
function mapMeasurementToDisplay(measurement, displaySetService) {
|
||||
const { referenceSeriesUID } = measurement;
|
||||
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(referenceSeriesUID);
|
||||
|
||||
if (!displaySets[0]?.instances) {
|
||||
throw new Error('The tracked measurements panel should only be tracking "stack" displaySets.');
|
||||
}
|
||||
|
||||
const { findingSites, finding, label: baseLabel, displayText: baseDisplayText } = measurement;
|
||||
|
||||
const firstSite = findingSites?.[0];
|
||||
const label = baseLabel || finding?.text || firstSite?.text || '(empty)';
|
||||
|
||||
// Initialize displayText with the structure used in Length.ts and CobbAngle.ts
|
||||
const displayText = {
|
||||
primary: [],
|
||||
secondary: baseDisplayText?.secondary || [],
|
||||
};
|
||||
|
||||
// Add baseDisplayText to primary if it exists
|
||||
if (baseDisplayText) {
|
||||
displayText.primary.push(...baseDisplayText.primary);
|
||||
}
|
||||
|
||||
// Add finding sites to primary
|
||||
if (findingSites) {
|
||||
findingSites.forEach(site => {
|
||||
if (site?.text && site.text !== label) {
|
||||
displayText.primary.push(site.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add finding to primary if it's different from the label
|
||||
if (finding && finding.text && finding.text !== label) {
|
||||
displayText.primary.push(finding.text);
|
||||
}
|
||||
|
||||
return {
|
||||
...measurement,
|
||||
displayText,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom hook that provides mapped measurements based on the given services and filters.
|
||||
*
|
||||
* @param {Object} servicesManager - The services manager object.
|
||||
* @param {Object} options - The options for filtering and mapping measurements.
|
||||
* @param {Function} options.measurementFilter - Optional function to filter measurements.
|
||||
* @param {Object} options.valueTypes - The value types for mapping measurements.
|
||||
* @returns {Array} An array of mapped and filtered measurements.
|
||||
*/
|
||||
export function useMeasurements(servicesManager, { measurementFilter }) {
|
||||
const { measurementService, displaySetService } = servicesManager.services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateDisplayMeasurements = () => {
|
||||
let measurements = measurementService.getMeasurements();
|
||||
if (measurementFilter) {
|
||||
measurements = measurements.filter(measurementFilter);
|
||||
}
|
||||
const mappedMeasurements = measurements.map(m =>
|
||||
mapMeasurementToDisplay(m, displaySetService)
|
||||
);
|
||||
setDisplayMeasurements(prevMeasurements => {
|
||||
if (JSON.stringify(prevMeasurements) !== JSON.stringify(mappedMeasurements)) {
|
||||
return mappedMeasurements;
|
||||
}
|
||||
return prevMeasurements;
|
||||
});
|
||||
};
|
||||
|
||||
const debouncedUpdate = debounce(updateDisplayMeasurements, 100);
|
||||
|
||||
updateDisplayMeasurements();
|
||||
|
||||
const events = [
|
||||
measurementService.EVENTS.MEASUREMENT_ADDED,
|
||||
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
|
||||
measurementService.EVENTS.MEASUREMENT_UPDATED,
|
||||
measurementService.EVENTS.MEASUREMENT_REMOVED,
|
||||
measurementService.EVENTS.MEASUREMENTS_CLEARED,
|
||||
];
|
||||
|
||||
const subscriptions = events.map(
|
||||
evt => measurementService.subscribe(evt, debouncedUpdate).unsubscribe
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => unsub());
|
||||
debouncedUpdate.cancel();
|
||||
};
|
||||
}, [measurementService, measurementFilter, displaySetService]);
|
||||
|
||||
return displayMeasurements;
|
||||
}
|
||||
148
extensions/cornerstone/src/hooks/useSegmentations.ts
Normal file
148
extensions/cornerstone/src/hooks/useSegmentations.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { roundNumber } from '@ohif/core/src/utils';
|
||||
import { SegmentationData } from '../services/SegmentationService/SegmentationService';
|
||||
|
||||
function mapSegmentationToDisplay(segmentation, customizationService) {
|
||||
const { label, segments } = segmentation;
|
||||
|
||||
// Get the readable text mapping once
|
||||
const { readableText: readableTextMap } = customizationService.getCustomization(
|
||||
'PanelSegmentation.readableText',
|
||||
{}
|
||||
);
|
||||
|
||||
// Helper function to recursively map cachedStats to readable display text
|
||||
function mapStatsToDisplay(stats, indent = 0) {
|
||||
const primary = [];
|
||||
const indentation = ' '.repeat(indent);
|
||||
|
||||
for (const key in stats) {
|
||||
if (Object.prototype.hasOwnProperty.call(stats, key)) {
|
||||
const value = stats[key];
|
||||
const readableText = readableTextMap?.[key];
|
||||
|
||||
if (!readableText) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Add empty row before category (except for the first category)
|
||||
if (primary.length > 0) {
|
||||
primary.push('');
|
||||
}
|
||||
// Add category title
|
||||
primary.push(`${indentation}${readableText}`);
|
||||
// Recursively handle nested objects
|
||||
primary.push(...mapStatsToDisplay(value, indent + 1));
|
||||
} else {
|
||||
// For non-nested values, don't add empty rows
|
||||
primary.push(`${indentation}${readableText}: ${roundNumber(value, 2)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return primary;
|
||||
}
|
||||
|
||||
// Get customization for display text mapping
|
||||
const displayTextMapper = segment => {
|
||||
const defaultDisplay = {
|
||||
primary: [],
|
||||
secondary: [],
|
||||
};
|
||||
|
||||
// If the segment has cachedStats, map it to readable text
|
||||
if (segment.cachedStats) {
|
||||
const primary = mapStatsToDisplay(segment.cachedStats);
|
||||
defaultDisplay.primary = primary;
|
||||
}
|
||||
|
||||
return defaultDisplay;
|
||||
};
|
||||
|
||||
const updatedSegments = {};
|
||||
|
||||
Object.entries(segments).forEach(([segmentIndex, segment]) => {
|
||||
updatedSegments[segmentIndex] = {
|
||||
...segment,
|
||||
displayText: displayTextMapper(segment),
|
||||
};
|
||||
});
|
||||
|
||||
// Map the segments and apply the display text mapper
|
||||
return {
|
||||
...segmentation,
|
||||
label,
|
||||
segments: updatedSegments,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook that provides segmentation data.
|
||||
* @param options - The options object.
|
||||
* @param options.servicesManager - The services manager object.
|
||||
* @param options.subscribeToDataModified - Whether to subscribe to segmentation data modifications.
|
||||
* @param options.debounceTime - Debounce time in milliseconds for updates.
|
||||
* @returns An array of segmentation data.
|
||||
*/
|
||||
export function useSegmentations({
|
||||
servicesManager,
|
||||
subscribeToDataModified = false,
|
||||
debounceTime = 0,
|
||||
}: withAppTypes<{ debounceTime?: number }>): SegmentationData[] {
|
||||
const { segmentationService, customizationService } = servicesManager.services;
|
||||
|
||||
const [segmentations, setSegmentations] = useState<SegmentationData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
|
||||
if (!segmentations?.length) {
|
||||
setSegmentations([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedSegmentations = segmentations.map(segmentation =>
|
||||
mapSegmentationToDisplay(segmentation, customizationService)
|
||||
);
|
||||
|
||||
setSegmentations(mappedSegmentations);
|
||||
};
|
||||
|
||||
const debouncedUpdate =
|
||||
debounceTime > 0 ? debounce(update, debounceTime, { leading: true, trailing: true }) : update;
|
||||
|
||||
update();
|
||||
|
||||
const subscriptions = [
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
debouncedUpdate
|
||||
),
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
||||
debouncedUpdate
|
||||
),
|
||||
];
|
||||
|
||||
if (subscribeToDataModified) {
|
||||
subscriptions.push(
|
||||
segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
debouncedUpdate
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(subscription => subscription.unsubscribe());
|
||||
if (debounceTime > 0) {
|
||||
debouncedUpdate.cancel();
|
||||
}
|
||||
};
|
||||
}, [segmentationService, customizationService, debounceTime, subscribeToDataModified]);
|
||||
|
||||
return segmentations;
|
||||
}
|
||||
Reference in New Issue
Block a user