Compare commits
3 Commits
v3.13.0-be
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bbe37ff388 | |||
|
|
f7612cdd0a | ||
|
|
125a174298 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
"name": "@ohif/extension-cornerstone-dicom-pmap",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "DICOM Parametric Map read workflow",
|
"description": "DICOM Parametric Map read workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "DICOM RT read workflow",
|
"description": "DICOM RT read workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "DICOM SEG read workflow",
|
"description": "DICOM SEG read workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for 4D volumes data",
|
"description": "OHIF extension for 4D volumes data",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata';
|
import { utilities as csMetadataUtilities } from '@cornerstonejs/metadata';
|
||||||
import { Types } from '@ohif/core';
|
import { Types, AnnotationPersistenceService } from '@ohif/core';
|
||||||
import Enums from './enums';
|
import Enums from './enums';
|
||||||
|
|
||||||
import init from './init';
|
import init from './init';
|
||||||
@ -216,6 +216,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
servicesManager.registerService(CornerstoneCacheService.REGISTRATION);
|
||||||
servicesManager.registerService(ColorbarService.REGISTRATION);
|
servicesManager.registerService(ColorbarService.REGISTRATION);
|
||||||
servicesManager.registerService(ViewedDataService.REGISTRATION);
|
servicesManager.registerService(ViewedDataService.REGISTRATION);
|
||||||
|
servicesManager.registerService(AnnotationPersistenceService.REGISTRATION);
|
||||||
|
|
||||||
const { syncGroupService } = servicesManager.services;
|
const { syncGroupService } = servicesManager.services;
|
||||||
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
syncGroupService.registerCustomSynchronizer('frameview', createFrameViewSynchronizer);
|
||||||
|
|||||||
@ -462,6 +462,12 @@ const connectMeasurementServiceToTools = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
|
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID, metadata } = measurement;
|
||||||
|
const persistedAnnotation = data?.annotation || {};
|
||||||
|
const persistedData = persistedAnnotation.data || {};
|
||||||
|
const persistedHandles = persistedData.handles || {};
|
||||||
|
const handlePoints = Array.isArray(persistedHandles.points)
|
||||||
|
? persistedHandles.points.filter(point => Array.isArray(point) && point.length >= 2)
|
||||||
|
: [];
|
||||||
|
|
||||||
const instance = DicomMetadataStore.getInstance(
|
const instance = DicomMetadataStore.getInstance(
|
||||||
referenceStudyUID,
|
referenceStudyUID,
|
||||||
@ -506,11 +512,11 @@ const connectMeasurementServiceToTools = ({
|
|||||||
* Don't remove this destructuring of data here.
|
* Don't remove this destructuring of data here.
|
||||||
* This is used to pass annotation specific data forward e.g. contour
|
* This is used to pass annotation specific data forward e.g. contour
|
||||||
*/
|
*/
|
||||||
...(data.annotation.data || {}),
|
...(persistedData || {}),
|
||||||
text: data.annotation.data.text,
|
text: persistedData.text,
|
||||||
handles: { ...data.annotation.data.handles },
|
handles: { ...persistedHandles, points: handlePoints },
|
||||||
cachedStats: { ...data.annotation.data.cachedStats },
|
cachedStats: { ...(persistedData.cachedStats || {}) },
|
||||||
label: data.annotation.data.label,
|
label: persistedData.label,
|
||||||
frameNumber,
|
frameNumber,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -534,7 +540,9 @@ const connectMeasurementServiceToTools = ({
|
|||||||
commandsManager.run('cancelMeasurement');
|
commandsManager.run('cancelMeasurement');
|
||||||
|
|
||||||
const removedAnnotation = annotation.state.getAnnotation(removedMeasurementId);
|
const removedAnnotation = annotation.state.getAnnotation(removedMeasurementId);
|
||||||
removeAnnotation(removedMeasurementId);
|
if (removedAnnotation) {
|
||||||
|
removeAnnotation(removedMeasurementId);
|
||||||
|
}
|
||||||
// Ensure `removedAnnotation` is available before triggering the memo,
|
// Ensure `removedAnnotation` is available before triggering the memo,
|
||||||
// as it can be undefined during an undo operation
|
// as it can be undefined during an undo operation
|
||||||
if (removedAnnotation) {
|
if (removedAnnotation) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-default",
|
"name": "@ohif/extension-default",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Common/default features and functionality for basic image viewing",
|
"description": "Common/default features and functionality for basic image viewing",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-microscopy",
|
"name": "@ohif/extension-dicom-microscopy",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for DICOM microscopy",
|
"description": "OHIF extension for DICOM microscopy",
|
||||||
"author": "Bill Wallace, md-prog",
|
"author": "Bill Wallace, md-prog",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-pdf",
|
"name": "@ohif/extension-dicom-pdf",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for PDF display",
|
"description": "OHIF extension for PDF display",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-video",
|
"name": "@ohif/extension-dicom-video",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for video display",
|
"description": "OHIF extension for video display",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-measurement-tracking",
|
"name": "@ohif/extension-measurement-tracking",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Tracking features and functionality for basic image viewing",
|
"description": "Tracking features and functionality for basic image viewing",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-test",
|
"name": "@ohif/extension-test",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension used inside e2e testing",
|
"description": "OHIF extension used inside e2e testing",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-tmtv",
|
"name": "@ohif/extension-tmtv",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF extension for Total Metabolic Tumor Volume",
|
"description": "OHIF extension for Total Metabolic Tumor Volume",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-ultrasound-pleura-bline",
|
"name": "@ohif/extension-ultrasound-pleura-bline",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "Rodrigo Basilio",
|
"author": "Rodrigo Basilio",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-basic-dev-mode",
|
"name": "@ohif/mode-basic-dev-mode",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Basic OHIF Viewer Using Cornerstone",
|
"description": "Basic OHIF Viewer Using Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-test",
|
"name": "@ohif/mode-test",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Basic mode for testing",
|
"description": "Basic mode for testing",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-basic",
|
"name": "@ohif/mode-basic",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "A basic mode used to build other modes on top of",
|
"description": "A basic mode used to build other modes on top of",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-longitudinal",
|
"name": "@ohif/mode-longitudinal",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Longitudinal Workflow",
|
"description": "Longitudinal Workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-microscopy",
|
"name": "@ohif/mode-microscopy",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF mode for DICOM microscopy",
|
"description": "OHIF mode for DICOM microscopy",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-preclinical-4d",
|
"name": "@ohif/mode-preclinical-4d",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "4D Workflow",
|
"description": "4D Workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-segmentation",
|
"name": "@ohif/mode-segmentation",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "OHIF segmentation mode which enables labelmap segmentation read/edit/export",
|
"description": "OHIF segmentation mode which enables labelmap segmentation read/edit/export",
|
||||||
"author": "@ohif",
|
"author": "@ohif",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-tmtv",
|
"name": "@ohif/mode-tmtv",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Total Metabolic Tumor Volume Workflow",
|
"description": "Total Metabolic Tumor Volume Workflow",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/mode-ultrasound-pleura-bline",
|
"name": "@ohif/mode-ultrasound-pleura-bline",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Allows users to annotate ultrasound images with pleura B-line annotations.",
|
"description": "Allows users to annotate ultrasound images with pleura B-line annotations.",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -124,5 +124,5 @@
|
|||||||
"git add"
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "3.13.0-beta.124"
|
"version": "3.13.0-beta.125"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/app",
|
"name": "@ohif/app",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"productVersion": "3.4.0",
|
"productVersion": "3.4.0",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/cli",
|
"name": "@ohif/cli",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "A CLI to bootstrap new OHIF extension or mode",
|
"description": "A CLI to bootstrap new OHIF extension or mode",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { keywords } from './enums/index.js';
|
import { keywords } from './enums/index.js';
|
||||||
import { validateYarn, addExtensionToConfig, addModeToConfig } from './utils/index.js';
|
import { validatePnpm, addExtensionToConfig, addModeToConfig } from './utils/index.js';
|
||||||
|
|
||||||
async function linkPackage(packageDir, options, addToConfig, keyword) {
|
async function linkPackage(packageDir, options, addToConfig, keyword) {
|
||||||
const { viewerDirectory } = options;
|
const { viewerDirectory } = options;
|
||||||
@ -22,19 +22,18 @@ async function linkPackage(packageDir, options, addToConfig, keyword) {
|
|||||||
|
|
||||||
const version = packageJSON.version;
|
const version = packageJSON.version;
|
||||||
|
|
||||||
// make sure yarn is installed
|
// make sure pnpm is installed
|
||||||
await validateYarn();
|
await validatePnpm();
|
||||||
|
|
||||||
// change directory to packageDir and execute yarn link
|
// resolve the package directory before changing the working directory
|
||||||
process.chdir(packageDir);
|
const resolvedPackageDir = path.resolve(packageDir);
|
||||||
|
|
||||||
let results;
|
// change directory to the OHIF Platform root and link the local package there.
|
||||||
results = await execa(`yarn`, ['link']);
|
// Linking mutates the lockfile, so disable the workspace's frozen-lockfile
|
||||||
|
// default for this call (pnpm link rejects --no-frozen-lockfile).
|
||||||
// change directory to OHIF Platform root and execute yarn link
|
|
||||||
process.chdir(`${viewerDirectory}/../..`);
|
process.chdir(`${viewerDirectory}/../..`);
|
||||||
|
|
||||||
results = await execa(`yarn`, ['link', packageName]);
|
let results = await execa('pnpm', ['link', resolvedPackageDir, '--config.frozen-lockfile=false']);
|
||||||
console.log(results.stdout);
|
console.log(results.stdout);
|
||||||
|
|
||||||
// Add the node_modules of the linked package so that webpack
|
// Add the node_modules of the linked package so that webpack
|
||||||
@ -63,7 +62,7 @@ async function linkPackage(packageDir, options, addToConfig, keyword) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// run prettier on the webpack config
|
// run prettier on the webpack config
|
||||||
results = await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
|
results = await execa('pnpm', ['exec', 'prettier', '--write', webpackPwaPath]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkExtension(packageDir, options) {
|
function linkExtension(packageDir, options) {
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { validateYarn, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js';
|
import { validatePnpm, removeExtensionFromConfig, removeModeFromConfig } from './utils/index.js';
|
||||||
|
|
||||||
const linkPackage = async (packageName, options, removeFromConfig) => {
|
const linkPackage = async (packageName, options, removeFromConfig) => {
|
||||||
const { viewerDirectory } = options;
|
const { viewerDirectory } = options;
|
||||||
|
|
||||||
// make sure yarn is installed
|
// make sure pnpm is installed
|
||||||
await validateYarn();
|
await validatePnpm();
|
||||||
|
|
||||||
// change directory to OHIF Platform root and execute yarn link
|
// change directory to the OHIF Platform root, where the package was linked.
|
||||||
process.chdir(viewerDirectory);
|
// Unlinking mutates the lockfile, so disable the workspace's frozen-lockfile
|
||||||
|
// default for this call (pnpm unlink rejects --no-frozen-lockfile).
|
||||||
|
process.chdir(`${viewerDirectory}/../..`);
|
||||||
|
|
||||||
const results = await execa(`yarn`, ['unlink', packageName]);
|
const results = await execa('pnpm', ['unlink', packageName, '--config.frozen-lockfile=false']);
|
||||||
console.log(results.stdout);
|
console.log(results.stdout);
|
||||||
|
|
||||||
const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js');
|
const webpackPwaPath = path.join(viewerDirectory, '.webpack', 'webpack.pwa.js');
|
||||||
@ -23,7 +25,7 @@ const linkPackage = async (packageName, options, removeFromConfig) => {
|
|||||||
removeFromConfig(packageName);
|
removeFromConfig(packageName);
|
||||||
|
|
||||||
// run prettier on the webpack config
|
// run prettier on the webpack config
|
||||||
await execa(`yarn`, ['prettier', '--write', webpackPwaPath]);
|
await execa('pnpm', ['exec', 'prettier', '--write', webpackPwaPath]);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function removePathFromWebpackConfig(webpackConfigPath, packageName) {
|
async function removePathFromWebpackConfig(webpackConfigPath, packageName) {
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import editPackageJson from './editPackageJson.js';
|
|||||||
import createLicense from './createLicense.js';
|
import createLicense from './createLicense.js';
|
||||||
import createReadme from './createReadme.js';
|
import createReadme from './createReadme.js';
|
||||||
import prettyPrint from './prettyPrint.js';
|
import prettyPrint from './prettyPrint.js';
|
||||||
import validateYarn from './validateYarn.js';
|
import validatePnpm from './validatePnpm.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getYarnInfo,
|
getYarnInfo,
|
||||||
@ -43,5 +43,5 @@ export {
|
|||||||
createLicense,
|
createLicense,
|
||||||
createReadme,
|
createReadme,
|
||||||
prettyPrint,
|
prettyPrint,
|
||||||
validateYarn,
|
validatePnpm,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
|
|
||||||
export default async function validateYarn() {
|
export default async function validatePnpm() {
|
||||||
try {
|
try {
|
||||||
await execa('yarn', ['--version']);
|
await execa('pnpm', ['--version']);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(
|
console.log(
|
||||||
'%s Yarn is not installed, please install it before linking your extension',
|
'%s pnpm is not installed, please install it before linking your extension',
|
||||||
chalk.red.bold('ERROR')
|
chalk.red.bold('ERROR')
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
MultiMonitorService,
|
MultiMonitorService,
|
||||||
|
AnnotationPersistenceService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||||
@ -91,6 +92,7 @@ const OHIF = {
|
|||||||
useActiveViewportDisplaySets,
|
useActiveViewportDisplaySets,
|
||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
|
AnnotationPersistenceService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -138,6 +140,7 @@ export {
|
|||||||
PanelService,
|
PanelService,
|
||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
|
AnnotationPersistenceService,
|
||||||
useSystem,
|
useSystem,
|
||||||
useActiveViewportDisplaySets,
|
useActiveViewportDisplaySets,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,259 @@
|
|||||||
|
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||||
|
|
||||||
|
const mockAnnotationManager = { addAnnotation: jest.fn() };
|
||||||
|
const mockAnnotations = new Map<string, any>();
|
||||||
|
|
||||||
|
jest.mock('@cornerstonejs/tools', () => ({
|
||||||
|
annotation: {
|
||||||
|
state: {
|
||||||
|
getAnnotationManager: jest.fn(() => mockAnnotationManager),
|
||||||
|
getAnnotation: jest.fn((uid: string) => mockAnnotations.get(uid)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
class FakeMeasurementService {
|
||||||
|
public EVENTS = {
|
||||||
|
MEASUREMENT_ADDED: 'MEASUREMENT_ADDED',
|
||||||
|
MEASUREMENT_UPDATED: 'MEASUREMENT_UPDATED',
|
||||||
|
MEASUREMENT_REMOVED: 'MEASUREMENT_REMOVED',
|
||||||
|
};
|
||||||
|
|
||||||
|
private listeners = new Map<string, Array<(event: any) => void>>();
|
||||||
|
public measurements = new Map<string, any>();
|
||||||
|
public addOrUpdateMeasurement = jest.fn((measurement: any) => {
|
||||||
|
this.measurements.set(measurement.uid, measurement);
|
||||||
|
return measurement.uid;
|
||||||
|
});
|
||||||
|
|
||||||
|
public getSource = jest.fn((name: string, version: string) => {
|
||||||
|
return { name, version, uid: 'test-source-uid' };
|
||||||
|
});
|
||||||
|
|
||||||
|
public subscribe(eventName: string, callback: (event: any) => void) {
|
||||||
|
if (!this.listeners.has(eventName)) {
|
||||||
|
this.listeners.set(eventName, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(eventName).push(callback);
|
||||||
|
return { unsubscribe: () => undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit(eventName: string, payload: any) {
|
||||||
|
this.listeners.get(eventName)?.forEach(callback => callback(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeDisplaySetService {
|
||||||
|
public EVENTS = {
|
||||||
|
DISPLAY_SETS_ADDED: 'DISPLAY_SETS_ADDED',
|
||||||
|
};
|
||||||
|
|
||||||
|
private listeners = new Map<string, Array<(event: any) => void>>();
|
||||||
|
|
||||||
|
public subscribe(eventName: string, callback: (event: any) => void) {
|
||||||
|
if (!this.listeners.has(eventName)) {
|
||||||
|
this.listeners.set(eventName, []);
|
||||||
|
}
|
||||||
|
this.listeners.get(eventName).push(callback);
|
||||||
|
return { unsubscribe: () => undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
public emit(eventName: string, payload: any) {
|
||||||
|
this.listeners.get(eventName)?.forEach(callback => callback(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeBackend {
|
||||||
|
public records: any[] = [];
|
||||||
|
|
||||||
|
public save(record: any) {
|
||||||
|
this.records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(record: any) {
|
||||||
|
this.records = this.records.filter(existing => existing.uid !== record.uid);
|
||||||
|
this.records.push(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete(uid: string) {
|
||||||
|
this.records = this.records.filter(existing => existing.uid !== uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public load(studyInstanceUID: string) {
|
||||||
|
return this.records.filter(entry => entry.studyInstanceUID === studyInstanceUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AnnotationPersistenceService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAnnotationManager.addAnnotation.mockClear();
|
||||||
|
mockAnnotations.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists created measurements and restores them for a study', async () => {
|
||||||
|
const backend = new FakeBackend();
|
||||||
|
const measurementService = new FakeMeasurementService();
|
||||||
|
const displaySetService = new FakeDisplaySetService();
|
||||||
|
const service = new AnnotationPersistenceService(
|
||||||
|
backend as any,
|
||||||
|
{
|
||||||
|
services: {
|
||||||
|
measurementService: measurementService as any,
|
||||||
|
displaySetService: displaySetService as any,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
);
|
||||||
|
|
||||||
|
const measurement = {
|
||||||
|
uid: 'measurement-1',
|
||||||
|
toolName: 'Length',
|
||||||
|
label: 'Length',
|
||||||
|
referenceStudyUID: 'study-1',
|
||||||
|
referenceSeriesUID: 'series-1',
|
||||||
|
referencedImageId: '1.2.3.4',
|
||||||
|
frameNumber: 1,
|
||||||
|
metadata: { toolName: 'Length', referencedImageId: '1.2.3.4' },
|
||||||
|
data: { cachedStats: {} },
|
||||||
|
points: [
|
||||||
|
[1, 2, 3],
|
||||||
|
[4, 5, 6],
|
||||||
|
],
|
||||||
|
source: { name: 'cornerstone', version: '3d', uid: 'source-1' },
|
||||||
|
isLocked: false,
|
||||||
|
isVisible: true,
|
||||||
|
displayText: { primary: ['1 mm'], secondary: [] },
|
||||||
|
type: 'value_type::polyline',
|
||||||
|
};
|
||||||
|
|
||||||
|
const annotation = {
|
||||||
|
annotationUID: 'measurement-1',
|
||||||
|
metadata: { toolName: 'Length', referencedImageId: '1.2.3.4' },
|
||||||
|
data: {
|
||||||
|
label: 'Length',
|
||||||
|
handles: {
|
||||||
|
points: [
|
||||||
|
[1, 2, 3],
|
||||||
|
[4, 5, 6],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
cachedStats: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockAnnotations.set('measurement-1', annotation);
|
||||||
|
|
||||||
|
measurementService.emit(measurementService.EVENTS.MEASUREMENT_ADDED, {
|
||||||
|
source: measurement.source,
|
||||||
|
measurement,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(backend.records).toHaveLength(1);
|
||||||
|
expect(backend.records[0].studyInstanceUID).toBe('study-1');
|
||||||
|
expect(backend.records[0].annotation.annotationUID).toBe('measurement-1');
|
||||||
|
|
||||||
|
backend.save({
|
||||||
|
uid: 'measurement-1',
|
||||||
|
studyInstanceUID: 'study-1',
|
||||||
|
annotation,
|
||||||
|
measurement,
|
||||||
|
});
|
||||||
|
backend.records = backend.records.filter(entry => entry.uid === 'measurement-1');
|
||||||
|
await service.restorePersistedAnnotations('study-1');
|
||||||
|
|
||||||
|
expect(measurementService.addOrUpdateMeasurement).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ uid: 'measurement-1' }),
|
||||||
|
expect.objectContaining({
|
||||||
|
annotation: expect.objectContaining({ annotationUID: 'measurement-1' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the correct registered source on restore and skips re-saving', async () => {
|
||||||
|
const backend = new FakeBackend();
|
||||||
|
const measurementService = new FakeMeasurementService();
|
||||||
|
const displaySetService = new FakeDisplaySetService();
|
||||||
|
const service = new AnnotationPersistenceService(
|
||||||
|
backend as any,
|
||||||
|
{
|
||||||
|
services: {
|
||||||
|
measurementService: measurementService as any,
|
||||||
|
displaySetService: displaySetService as any,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
);
|
||||||
|
|
||||||
|
const measurement = {
|
||||||
|
uid: 'measurement-2',
|
||||||
|
referenceStudyUID: 'study-2',
|
||||||
|
source: { name: 'Cornerstone3DTools', version: '0.1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
backend.save({
|
||||||
|
uid: 'measurement-2',
|
||||||
|
studyInstanceUID: 'study-2',
|
||||||
|
measurement,
|
||||||
|
annotation: { annotationUID: 'measurement-2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveSpy = jest.spyOn(backend, 'save');
|
||||||
|
const updateSpy = jest.spyOn(backend, 'update');
|
||||||
|
await service.restorePersistedAnnotations('study-2');
|
||||||
|
|
||||||
|
expect(measurementService.getSource).toHaveBeenCalledWith('Cornerstone3DTools', '0.1');
|
||||||
|
expect(measurementService.addOrUpdateMeasurement).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
uid: 'measurement-2',
|
||||||
|
source: expect.objectContaining({ name: 'Cornerstone3DTools', uid: 'test-source-uid' }),
|
||||||
|
}),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate MEASUREMENT_ADDED being fired by addOrUpdateMeasurement
|
||||||
|
measurementService.emit(measurementService.EVENTS.MEASUREMENT_ADDED, {
|
||||||
|
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not re-save during restore
|
||||||
|
expect(saveSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Simulate MEASUREMENT_UPDATED
|
||||||
|
measurementService.emit(measurementService.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
|
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second update should save/update
|
||||||
|
measurementService.emit(measurementService.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
|
measurement: { uid: 'measurement-2', referenceStudyUID: 'study-2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a tick for async updates to run
|
||||||
|
await new Promise(process.nextTick);
|
||||||
|
|
||||||
|
expect(updateSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes measurement from backend when MEASUREMENT_REMOVED fires', async () => {
|
||||||
|
const backend = new FakeBackend();
|
||||||
|
const measurementService = new FakeMeasurementService();
|
||||||
|
const service = new AnnotationPersistenceService(
|
||||||
|
backend as any,
|
||||||
|
{
|
||||||
|
services: {
|
||||||
|
measurementService: measurementService as any,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
);
|
||||||
|
|
||||||
|
backend.records.push({ uid: 'measurement-3', studyInstanceUID: 'study-3' });
|
||||||
|
|
||||||
|
measurementService.emit(measurementService.EVENTS.MEASUREMENT_REMOVED, {
|
||||||
|
measurement: 'measurement-3',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a tick for async delete to run
|
||||||
|
await new Promise(process.nextTick);
|
||||||
|
|
||||||
|
expect(backend.records).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -0,0 +1,304 @@
|
|||||||
|
import { annotation } from '@cornerstonejs/tools';
|
||||||
|
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||||
|
|
||||||
|
export interface AnnotationPersistenceBackend {
|
||||||
|
save(record: Record<string, any>): Promise<void> | void;
|
||||||
|
update(record: Record<string, any>): Promise<void> | void;
|
||||||
|
delete(uid: string): Promise<void> | void;
|
||||||
|
load(studyInstanceUID: string): Promise<any[]> | any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnnotationPersistenceServiceOptions {
|
||||||
|
services: {
|
||||||
|
measurementService: any;
|
||||||
|
displaySetService?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnnotationPersistenceRecord = {
|
||||||
|
uid: string;
|
||||||
|
studyInstanceUID: string;
|
||||||
|
seriesInstanceUID?: string;
|
||||||
|
annotation: Record<string, any>;
|
||||||
|
measurement: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class AnnotationPersistenceService {
|
||||||
|
public static REGISTRATION = {
|
||||||
|
name: 'annotationPersistenceService',
|
||||||
|
altName: 'AnnotationPersistenceService',
|
||||||
|
create: ({ servicesManager }: { servicesManager: any }) => {
|
||||||
|
const backend = new LocalStorageAnnotationPersistenceBackend();
|
||||||
|
return new AnnotationPersistenceService(backend, {
|
||||||
|
services: {
|
||||||
|
measurementService: servicesManager.services.measurementService,
|
||||||
|
displaySetService: servicesManager.services.displaySetService,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private backend: AnnotationPersistenceBackend;
|
||||||
|
private services: AnnotationPersistenceServiceOptions['services'];
|
||||||
|
private currentStudyInstanceUID?: string;
|
||||||
|
private restoredUIDs = new Set<string>();
|
||||||
|
|
||||||
|
constructor(backend: AnnotationPersistenceBackend, options: AnnotationPersistenceServiceOptions) {
|
||||||
|
this.backend = backend;
|
||||||
|
this.services = options.services;
|
||||||
|
this._bindEvents();
|
||||||
|
this._bindDisplaySetEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async saveAnnotation(measurement: Record<string, any>): Promise<void> {
|
||||||
|
if (!measurement?.uid || !measurement.referenceStudyUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = this._buildRecord(measurement);
|
||||||
|
try {
|
||||||
|
await this.backend.save(record);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Annotation persistence save failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateAnnotation(measurement: Record<string, any>): Promise<void> {
|
||||||
|
if (!measurement?.uid || !measurement.referenceStudyUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = this._buildRecord(measurement);
|
||||||
|
try {
|
||||||
|
await this.backend.update(record);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Annotation persistence update failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteAnnotation(uid: string): Promise<void> {
|
||||||
|
if (!uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.backend.delete(uid);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Annotation persistence delete failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restorePersistedAnnotations(studyInstanceUID: string): Promise<void> {
|
||||||
|
if (!studyInstanceUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentStudyInstanceUID = studyInstanceUID;
|
||||||
|
try {
|
||||||
|
const records = await this.backend.load(studyInstanceUID);
|
||||||
|
for (const record of records) {
|
||||||
|
if (!record?.measurement?.uid || this.restoredUIDs.has(record.measurement.uid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.restoredUIDs.add(record.measurement.uid);
|
||||||
|
this._restoreMeasurement(record);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Annotation persistence restore failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _bindEvents(): void {
|
||||||
|
const measurementService = this.services.measurementService;
|
||||||
|
if (!measurementService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementService.subscribe(measurementService.EVENTS.MEASUREMENT_ADDED, ({ measurement }) => {
|
||||||
|
if (this.restoredUIDs.has(measurement.uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saveAnnotation(measurement).catch(() => undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
measurementService.subscribe(
|
||||||
|
measurementService.EVENTS.MEASUREMENT_UPDATED,
|
||||||
|
({ measurement }) => {
|
||||||
|
if (this.restoredUIDs.has(measurement.uid)) {
|
||||||
|
this.restoredUIDs.delete(measurement.uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateAnnotation(measurement).catch(() => undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
measurementService.subscribe(
|
||||||
|
measurementService.EVENTS.MEASUREMENT_REMOVED,
|
||||||
|
({ measurement }) => {
|
||||||
|
this.deleteAnnotation(measurement).catch(() => undefined);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _bindDisplaySetEvents(): void {
|
||||||
|
const displaySetService = this.services.displaySetService;
|
||||||
|
if (!displaySetService?.subscribe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySetService.subscribe(
|
||||||
|
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
|
({ displaySetsAdded }) => {
|
||||||
|
const studyInstanceUIDs = displaySetsAdded?.map(displaySet => displaySet?.StudyInstanceUID);
|
||||||
|
studyInstanceUIDs?.forEach(studyInstanceUID => {
|
||||||
|
if (studyInstanceUID) {
|
||||||
|
this.restorePersistedAnnotations(studyInstanceUID).catch(() => undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildRecord(measurement: Record<string, any>): AnnotationPersistenceRecord {
|
||||||
|
const annotationManager = annotation.state.getAnnotationManager();
|
||||||
|
const annotationInstance = annotation.state.getAnnotation(measurement.uid);
|
||||||
|
const studyInstanceUID = measurement.referenceStudyUID;
|
||||||
|
const seriesInstanceUID = measurement.referenceSeriesUID;
|
||||||
|
|
||||||
|
const annotationPayload = annotationInstance || {
|
||||||
|
annotationUID: measurement.uid,
|
||||||
|
metadata: {
|
||||||
|
...measurement.metadata,
|
||||||
|
toolName: measurement.toolName,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: measurement.label,
|
||||||
|
handles: measurement.points ? { points: measurement.points } : undefined,
|
||||||
|
cachedStats: measurement.data || {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
uid: measurement.uid,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
annotation: annotationPayload,
|
||||||
|
measurement: {
|
||||||
|
...measurement,
|
||||||
|
annotationUID: measurement.uid,
|
||||||
|
metadata: {
|
||||||
|
...(measurement.metadata || {}),
|
||||||
|
toolName: measurement.toolName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _restoreMeasurement(record: AnnotationPersistenceRecord): void {
|
||||||
|
const measurementService = this.services.measurementService;
|
||||||
|
if (!measurementService) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistedSource = record.measurement.source;
|
||||||
|
const resolvedSource =
|
||||||
|
(persistedSource?.name && persistedSource?.version
|
||||||
|
? measurementService.getSource(persistedSource.name, persistedSource.version)
|
||||||
|
: null) ||
|
||||||
|
measurementService.getSource('Cornerstone3DTools', '0.1');
|
||||||
|
|
||||||
|
if (!resolvedSource) {
|
||||||
|
console.warn('Annotation persistence: no registered source found, skipping restore');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = {
|
||||||
|
...record.measurement,
|
||||||
|
uid: record.measurement.uid || record.uid,
|
||||||
|
referenceStudyUID: record.studyInstanceUID,
|
||||||
|
referenceSeriesUID: record.seriesInstanceUID,
|
||||||
|
source: resolvedSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.currentStudyInstanceUID &&
|
||||||
|
measurement.referenceStudyUID !== this.currentStudyInstanceUID
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAnnotation = this._normalizePersistedAnnotation(record.annotation);
|
||||||
|
const rebuiltAnnotation = this._buildRestoredAnnotation(measurement, normalizedAnnotation);
|
||||||
|
|
||||||
|
measurementService.addOrUpdateMeasurement?.(measurement, {
|
||||||
|
annotation: rebuiltAnnotation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _normalizePersistedAnnotation(
|
||||||
|
annotationPayload?: Record<string, any>
|
||||||
|
): Record<string, any> {
|
||||||
|
if (!annotationPayload) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedData = annotationPayload.data || {};
|
||||||
|
const normalizedHandles = normalizedData.handles || {};
|
||||||
|
const rawPoints = Array.isArray(normalizedHandles.points)
|
||||||
|
? normalizedHandles.points
|
||||||
|
: Array.isArray(normalizedData.points)
|
||||||
|
? normalizedData.points
|
||||||
|
: Array.isArray(annotationPayload.points)
|
||||||
|
? annotationPayload.points
|
||||||
|
: [];
|
||||||
|
const normalizedPoints = rawPoints.filter(point => Array.isArray(point) && point.length >= 2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...annotationPayload,
|
||||||
|
data: {
|
||||||
|
...normalizedData,
|
||||||
|
handles: {
|
||||||
|
...normalizedHandles,
|
||||||
|
points: normalizedPoints,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildRestoredAnnotation(
|
||||||
|
measurement: Record<string, any>,
|
||||||
|
annotationPayload?: Record<string, any>
|
||||||
|
): Record<string, any> {
|
||||||
|
const normalizedAnnotation = this._normalizePersistedAnnotation(annotationPayload);
|
||||||
|
const persistedData = normalizedAnnotation.data || {};
|
||||||
|
const persistedHandles = persistedData.handles || {};
|
||||||
|
const points =
|
||||||
|
Array.isArray(persistedHandles.points) && persistedHandles.points.length
|
||||||
|
? persistedHandles.points
|
||||||
|
: Array.isArray(measurement.points)
|
||||||
|
? measurement.points.filter(point => Array.isArray(point) && point.length >= 2)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
annotationUID: measurement.uid || normalizedAnnotation.annotationUID,
|
||||||
|
metadata: {
|
||||||
|
...(normalizedAnnotation.metadata || {}),
|
||||||
|
toolName: measurement.toolName || normalizedAnnotation.metadata?.toolName,
|
||||||
|
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
|
||||||
|
referencedImageId: measurement.referencedImageId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...(persistedData || {}),
|
||||||
|
label: persistedData.label || measurement.label,
|
||||||
|
handles: {
|
||||||
|
...persistedHandles,
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
cachedStats: { ...(persistedData.cachedStats || measurement.data || {}) },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
export interface AnnotationPersistenceBackend {
|
||||||
|
save(record: Record<string, any>): Promise<void> | void;
|
||||||
|
update(record: Record<string, any>): Promise<void> | void;
|
||||||
|
delete(uid: string): Promise<void> | void;
|
||||||
|
load(studyInstanceUID: string): Promise<any[]> | any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ohif.annotation.persistence';
|
||||||
|
|
||||||
|
export default class LocalStorageAnnotationPersistenceBackend
|
||||||
|
implements AnnotationPersistenceBackend
|
||||||
|
{
|
||||||
|
public save(record: Record<string, any>): void {
|
||||||
|
const entries = this._readEntries();
|
||||||
|
const nextEntries = entries.filter(existing => existing.uid !== record.uid);
|
||||||
|
nextEntries.push(record);
|
||||||
|
this._writeEntries(nextEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(record: Record<string, any>): void {
|
||||||
|
this.save(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete(uid: string): void {
|
||||||
|
const entries = this._readEntries().filter(existing => existing.uid !== uid);
|
||||||
|
this._writeEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public load(studyInstanceUID: string): any[] {
|
||||||
|
return this._readEntries().filter(entry => entry.studyInstanceUID === studyInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _readEntries(): Record<string, any>[] {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to read persisted annotations from storage:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _writeEntries(entries: Record<string, any>[]): void {
|
||||||
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||||
|
import type {
|
||||||
|
AnnotationPersistenceBackend,
|
||||||
|
AnnotationPersistenceRecord,
|
||||||
|
AnnotationPersistenceServiceOptions,
|
||||||
|
} from './AnnotationPersistenceService';
|
||||||
|
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AnnotationPersistenceBackend,
|
||||||
|
AnnotationPersistenceRecord,
|
||||||
|
AnnotationPersistenceServiceOptions,
|
||||||
|
};
|
||||||
|
export { LocalStorageAnnotationPersistenceBackend };
|
||||||
|
|
||||||
|
export default AnnotationPersistenceService;
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import AnnotationPersistenceService, {
|
||||||
|
AnnotationPersistenceServiceOptions,
|
||||||
|
} from './AnnotationPersistenceService';
|
||||||
|
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||||
|
|
||||||
|
const registration = {
|
||||||
|
name: 'annotationPersistenceService',
|
||||||
|
altName: 'AnnotationPersistenceService',
|
||||||
|
create: ({ servicesManager }: { servicesManager: any }) => {
|
||||||
|
const backend = new LocalStorageAnnotationPersistenceBackend();
|
||||||
|
return new AnnotationPersistenceService(backend, {
|
||||||
|
services: {
|
||||||
|
measurementService: servicesManager.services.measurementService,
|
||||||
|
displaySetService: servicesManager.services.displaySetService,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default registration;
|
||||||
@ -385,6 +385,45 @@ class MeasurementService extends PubSubService {
|
|||||||
return updatedMeasurement.uid;
|
return updatedMeasurement.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addOrUpdateMeasurement(measurement, restoreData?: any) {
|
||||||
|
if (!measurement?.uid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMeasurement = this.measurements.get(measurement.uid);
|
||||||
|
const restoredMeasurement = {
|
||||||
|
...(existingMeasurement || {}),
|
||||||
|
...measurement,
|
||||||
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
|
uid: measurement.uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.measurements.set(restoredMeasurement.uid, restoredMeasurement);
|
||||||
|
|
||||||
|
if (existingMeasurement) {
|
||||||
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
|
source: restoredMeasurement.source,
|
||||||
|
measurement: restoredMeasurement,
|
||||||
|
notYetUpdatedAtSource: false,
|
||||||
|
});
|
||||||
|
return restoredMeasurement.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||||
|
source: restoredMeasurement.source,
|
||||||
|
measurement: restoredMeasurement,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||||
|
source: restoredMeasurement.source,
|
||||||
|
measurement: restoredMeasurement,
|
||||||
|
data: restoreData || {},
|
||||||
|
dataSource: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return restoredMeasurement.uid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a raw measurement into a source so that it may be
|
* Add a raw measurement into a source so that it may be
|
||||||
* Converted to/from annotation in the same way. E.g. import serialized data
|
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import PanelService from './PanelService';
|
|||||||
import WorkflowStepsService from './WorkflowStepsService';
|
import WorkflowStepsService from './WorkflowStepsService';
|
||||||
import StudyPrefetcherService from './StudyPrefetcherService';
|
import StudyPrefetcherService from './StudyPrefetcherService';
|
||||||
import { MultiMonitorService } from './MultiMonitorService';
|
import { MultiMonitorService } from './MultiMonitorService';
|
||||||
|
import AnnotationPersistenceService from './AnnotationPersistenceService';
|
||||||
|
|
||||||
import type Services from '../types/Services';
|
import type Services from '../types/Services';
|
||||||
|
|
||||||
@ -46,4 +47,5 @@ export {
|
|||||||
PanelService,
|
PanelService,
|
||||||
WorkflowStepsService,
|
WorkflowStepsService,
|
||||||
StudyPrefetcherService,
|
StudyPrefetcherService,
|
||||||
|
AnnotationPersistenceService,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ohif-docs",
|
"name": "ohif-docs",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui-next",
|
"name": "@ohif/ui-next",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "Next version of OHIF Viewers UI, more customizable using shadcn/ui",
|
"description": "Next version of OHIF Viewers UI, more customizable using shadcn/ui",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ShepherdBase } from 'shepherd.js';
|
import { ShepherdBase } from 'shepherd.js';
|
||||||
import { offset, flip, shift, detectOverflow } from '@floating-ui/dom';
|
import { offset, flip, shift } from '@floating-ui/dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the list of tours that have been shown from localStorage.
|
* Retrieves the list of tours that have been shown from localStorage.
|
||||||
@ -51,41 +51,13 @@ const defaultShowHandler = (Shepherd: ShepherdBase) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom middleware for adjusting Shepherd step positioning when overflowing.
|
|
||||||
*
|
|
||||||
* @type {object}
|
|
||||||
* @property {string} name - The name of the middleware.
|
|
||||||
* @property {function} fn - The function that adjusts the position of the step when overflowing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const customMiddleware = {
|
|
||||||
name: 'customOverflowMiddleware',
|
|
||||||
async fn(state) {
|
|
||||||
const overflow = await detectOverflow(state, {
|
|
||||||
boundary: document.querySelector('body'),
|
|
||||||
padding: 24,
|
|
||||||
});
|
|
||||||
|
|
||||||
const xAdjustment =
|
|
||||||
overflow.left > 0 ? overflow.left : overflow.right > 0 ? -overflow.right : 0;
|
|
||||||
const yAdjustment =
|
|
||||||
overflow.top > 0 ? overflow.top : overflow.bottom > 0 ? -overflow.bottom : 0;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: state.x + xAdjustment,
|
|
||||||
y: state.y + yAdjustment,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Floating UI middleware for positioning steps in Shepherd.js.
|
* Default Floating UI middleware for positioning steps in Shepherd.js.
|
||||||
* Includes offset, shift, flip, and custom overflow middleware.
|
* Includes offset, shift, and flip middleware.
|
||||||
*
|
*
|
||||||
* @type {Array<object>}
|
* @type {Array<object>}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const middleware = [offset(15), shift(), flip(), customMiddleware];
|
const middleware = [offset(15), shift({ padding: 24 }), flip()];
|
||||||
|
|
||||||
export { hasTourBeenShown, markTourAsShown, middleware, defaultShowHandler };
|
export { hasTourBeenShown, markTourAsShown, middleware, defaultShowHandler };
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "3.13.0-beta.124",
|
"version": "3.13.0-beta.125",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user