This commit is contained in:
mario
2025-03-07 13:47:44 +07:00
commit c4efec5a14
3358 changed files with 303774 additions and 0 deletions

View File

@@ -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;
}

View 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;
}

View 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;
}