feat: cornerstone3D stack and volume viewports (#2787)
* feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock * feat: overlay component (#21) * fix default displayset options * add viewport overlay * apply review comments * feat: loading and orientation indicators (#22) * add loading indicator * add orientation marker initial work * apply review comments * fix: orientation markers (#23) * finished the orientation markers * fix various broken cypress tests * apply review comments * update yarn lock * feat: re-working measurement tracking mode with cornerstone3d (#2805) * feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: Add Measurement tracking mode with cs3D (#2789) * feat: first render for cornerstone3d tracked viewport * make tool active work * wip for SR extension * renamed dicom sr to cornerstone dicom sr * remove cornerstone from dicom pdf and video * move dicom sr logic to sr extension * feat: Add hydration for the length tool * fix SR display tool for length using cs3d * fix default config * fix: various bugs with sr viewport and tracking * fix promptying to continue tracking for when SR is created * feat: add keep trackign of unique identifiers * fix hydration for same imageIds * feat: Add SR toolGroup creation on modeEnter * feat: remove the need for separate mapper for SR hydration * add SR display for ellipse * handle hydration of elliptical ROI tool * remove cornerstone extension * add arrow mapping * feat: Add ArrowAnnotate SR display and hydration * apply review comments * apply review comments * move viewport labels to the viewportData * apply review comments * fix: integration cypress tests with cornerstone3D and add CINE tool (#2795) * fix: integration cypress tests with cornerstone3D * revert to addOrUpdate as it makes more sense * fix local drag and drop * fix tests * move dicomLoaderService to cornerstone extension * fix various import bugs * fix bug for local PT series * fix various unit tests * bump cs3d versions * add angle and magnify tool * bump dependencies to avoid broken peerDeps * feat: add initial work for capture using cs3d * feat: show annotations on the image capture * feat: add svg layer export * feat: Add CINE Tool * feat: remove unnecessary viewport rendering for cine state changes Co-authored-by: Erik Ziegler <erik.sweed@gmail.com> * docs: modify and improve documentation (#2800) * cleanup docs versionings * feat: Add all version explanations * version docs for 3.0 * wip for changing docs * wip for updated docs * add utility module documentation * fix demo with nohoisting of history * add slides and video to resources * apply review comments * fix: drag and drop SR into SR viewport (#2803) * update yarn lock Co-authored-by: Erik <erik.sweed@gmail.com> * update pathnames to match v3-stable * update the e2e pathname * fix: various bugs with regard to tracking workflow (#2811) * fix: various issues with measurement panel * fix: update default tool style for annotations * fix: annotatoin label getting removed * feat: Add backward compatibility for SR hydration with legacy cornerstone * fix: cursors and ellipse ROI max style * fix: ArrowAnnotate SRDisplay * apply review comments * bump package versions * fix: bugin rehydration of SR * fix: e2e tests * fix active viewport thickness and arrowTool ui * add readme for measurement tracking * use uploaded image for readme * add back images * try to fix e2e test * fix: window level presets hotkeys * Update README.md * update yarn lock * feat: volume api and TMTV mode (#2817) * feat: volumeAPI and TMTV mode * feat: use cs3d cache service to obtain viewportData * wip for volume api * wip: fusion viewport * feat: add blend mode option * wip for image scrollbar * fix drag and drop thumbnail * wip for image scrollbar for voluems * fix: element mismatch bug for scroll * feat: Add image scrollbar to volumes * feat: add syncGroups to volume api * feat: add tmtv mode initial setup * feat: add initial image options for the stack viewports * feat: Add custom load strategy for volume viewports via HP * apply review comments fix: Jump presets cs3d (#2812) * feat: Add JumpPreset to OHIF for Cornerstone3D * fix: accessing viewport service from servicesManager feat: volume API and TMTV mode (#2814) * fix: do not display overlays on mip viewports * feat: add optional disableCommands for toggle buttons * fix: toggleCrossharis * feat: config the crosshairs * feat: add PetSUV Panel for changing metadata * feat: initial work for the rectangleROIThreshold panel * wip: measurement service * roi threshold working * feat: Add displayText to segmentations * feat: add remove segmentation * feat: add csv export * feat: add RT export for annotations in tmtv mode * fix: fusion to use pt in tmtv mode and measuremet mappings * fix: various bugs * apply review comments * apply review comments * feat: add fusion color maps * add readme to tmtv mode * Update README.md * fix: try to fix build * fix unit tests * Update README.md * feat: add about to the panel * fix: changing strategy in roi panel * apply review comments * update package versions * wip for stackPrefetch * fix: cornerstone3d hydration and renaming (#2818) * update readme * renamed cornerstone extension * wip for renaming cornerstone3D variables * wip for renaming cornerstone3D variables * wip for fixing bugs for SR viewport * fix: jumpToMeasurement and initial label after hydration * fix: fileName capitalization * feat: use the new prefetch stack in the cs3d (#2820) * feat: use the new prefetch stack in the cs3d * use viewport scroll api for stack viewport * fix cine stop when scrollbar changes * feat: use new prefetch events * fix loading state to not show repeatedly * fix: various bugs for tmtv mode thresholding and new icons (#2823) * feat: make tmtv mode available in worklist * feat: add new icons for tmtv mode * feat: add fusion color icon * fix: parallel scale calculation * fix: bump Cornerstone to get large image support working * fix: Fix issues with CPU viewport flipping, including config files * fix: bump cornerstone to fix magnify tool * fix: Bump Cornerstone version to fix resetCamera issue in StackViewport * ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys * fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors * feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838) Also works with the compile time add that the existing cli uses, so that both build types work. Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes. * bump: dependency versions to fix hydration bugs (#2848) * bump: dcmjs version to fix hydration bugs * try to fix tests * bump dependency versions Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
668 files changed
+41669
-13410
No files matched your search
@@ -13,7 +13,6 @@ module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"dcmjs": "0.16.1",
|
||||
"dcmjs": "^0.24.5",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
@@ -42,6 +42,7 @@
|
||||
"webpack-merge": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.3"
|
||||
"@babel/runtime": "7.16.3",
|
||||
"@cornerstonejs/calculate-suv": "1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,8 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
SOPInstanceUID,
|
||||
} = instance;
|
||||
|
||||
instance.imageId = imageId;
|
||||
|
||||
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import {
|
||||
DicomMetadataStore,
|
||||
IWebApiDataSource,
|
||||
utils,
|
||||
errorHandler,
|
||||
classes,
|
||||
} from '@ohif/core';
|
||||
|
||||
import {
|
||||
mapParams,
|
||||
search as qidoSearch,
|
||||
@@ -7,12 +15,6 @@ import {
|
||||
processSeriesResults,
|
||||
} from './qido.js';
|
||||
import dcm4cheeReject from './dcm4cheeReject';
|
||||
import {
|
||||
DicomMetadataStore,
|
||||
IWebApiDataSource,
|
||||
utils,
|
||||
errorHandler,
|
||||
} from '@ohif/core';
|
||||
|
||||
import getImageId from './utils/getImageId';
|
||||
import dcmjs from 'dcmjs';
|
||||
@@ -31,6 +33,8 @@ const ImplementationClassUID =
|
||||
const ImplementationVersionName = 'OHIF-VIEWER-2.0.0';
|
||||
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name - Data source name
|
||||
@@ -94,7 +98,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
query: {
|
||||
studies: {
|
||||
mapParams: mapParams.bind(),
|
||||
search: async function (origParams) {
|
||||
search: async function(origParams) {
|
||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
qidoDicomWebClient.headers = headers;
|
||||
@@ -119,7 +123,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
},
|
||||
series: {
|
||||
// mapParams: mapParams.bind(),
|
||||
search: async function (studyInstanceUid) {
|
||||
search: async function(studyInstanceUid) {
|
||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||
if (headers) {
|
||||
qidoDicomWebClient.headers = headers;
|
||||
@@ -163,13 +167,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
|
||||
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
||||
*/
|
||||
directURL: (params) => {
|
||||
directURL: params => {
|
||||
const {
|
||||
instance,
|
||||
tag = "PixelData",
|
||||
defaultPath = "/pixeldata",
|
||||
defaultType = "video/mp4",
|
||||
singlepart: fetchPart = "video",
|
||||
tag = 'PixelData',
|
||||
defaultPath = '/pixeldata',
|
||||
defaultType = 'video/mp4',
|
||||
singlepart: fetchPart = 'video',
|
||||
} = params;
|
||||
const value = instance[tag];
|
||||
if (!value) return undefined;
|
||||
@@ -180,10 +184,15 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
value.DirectRetrieveURL = URL.createObjectURL(blob);
|
||||
return value.DirectRetrieveURL;
|
||||
}
|
||||
if (!singlepart || singlepart !== true && singlepart.indexOf(fetchPart) === -1) {
|
||||
if (
|
||||
!singlepart ||
|
||||
(singlepart !== true && singlepart.indexOf(fetchPart) === -1)
|
||||
) {
|
||||
if (value.retrieveBulkData) {
|
||||
return value.retrieveBulkData().then(arr => {
|
||||
value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType }));
|
||||
value.DirectRetrieveURL = URL.createObjectURL(
|
||||
new Blob([arr], { type: defaultType })
|
||||
);
|
||||
return value.DirectRetrieveURL;
|
||||
});
|
||||
}
|
||||
@@ -191,7 +200,11 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
} = instance;
|
||||
const BulkDataURI =
|
||||
(value && value.BulkDataURI) ||
|
||||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
||||
@@ -234,94 +247,27 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
);
|
||||
}
|
||||
|
||||
// Get Series
|
||||
const {
|
||||
seriesSummaryMetadata,
|
||||
seriesPromises,
|
||||
} = await retrieveStudyMetadata(
|
||||
wadoDicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
|
||||
/**
|
||||
* naturalizes the dataset, and adds a retrieve bulkdata method
|
||||
* to any values containing BulkDataURI.
|
||||
* @param {*} instance
|
||||
* @returns naturalized dataset, with retrieveBulkData methods
|
||||
*/
|
||||
const addRetrieveBulkData = instance => {
|
||||
const naturalized = naturalizeDataset(instance);
|
||||
Object.keys(naturalized).forEach(key => {
|
||||
const value = naturalized[key];
|
||||
// The value.Value will be set with the bulkdata read value
|
||||
// in which case it isn't necessary to re-read this.
|
||||
if (value && value.BulkDataURI && !value.Value) {
|
||||
// Provide a method to fetch bulkdata
|
||||
value.retrieveBulkData = () => {
|
||||
const options = {
|
||||
// The bulkdata fetches work with either multipart or
|
||||
// singlepart, so set multipart to false to let the server
|
||||
// decide which type to respond with.
|
||||
multipart: false,
|
||||
BulkDataURI: value.BulkDataURI,
|
||||
// The study instance UID is required if the bulkdata uri
|
||||
// is relative - that isn't disallowed by DICOMweb, but
|
||||
// isn't well specified in the standard, but is needed in
|
||||
// any implementation that stores static copies of the metadata
|
||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||
};
|
||||
return qidoDicomWebClient
|
||||
.retrieveBulkData(options)
|
||||
.then(val => {
|
||||
const ret = (val && val[0]) || undefined;
|
||||
value.Value = ret;
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
return naturalized;
|
||||
};
|
||||
|
||||
// Async load series, store as retrieved
|
||||
function storeInstances(instances) {
|
||||
const naturalizedInstances = instances.map(addRetrieveBulkData);
|
||||
|
||||
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||
}
|
||||
|
||||
function setSuccessFlag() {
|
||||
const study = DicomMetadataStore.getStudy(
|
||||
if (enableStudyLazyLoad) {
|
||||
return implementation._retrieveSeriesMetadataAsync(
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
);
|
||||
study.isLoaded = true;
|
||||
}
|
||||
|
||||
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
|
||||
// it manually here
|
||||
seriesSummaryMetadata.forEach(aSeries => {
|
||||
aSeries.StudyInstanceUID = StudyInstanceUID;
|
||||
});
|
||||
|
||||
DicomMetadataStore.addSeriesMetadata(
|
||||
seriesSummaryMetadata,
|
||||
return implementation._retrieveSeriesMetadataSync(
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
);
|
||||
|
||||
const numberOfSeries = seriesPromises.length;
|
||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
||||
const instances = await seriesPromise;
|
||||
storeInstances(instances);
|
||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
store: {
|
||||
dicom: async dataset => {
|
||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||
@@ -353,6 +299,187 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
await wadoDicomWebClient.storeInstances(options);
|
||||
},
|
||||
},
|
||||
_retrieveSeriesMetadataSync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient
|
||||
) => {
|
||||
const enableStudyLazyLoad = false;
|
||||
|
||||
// data is all SOPInstanceUIDs
|
||||
const data = await retrieveStudyMetadata(
|
||||
wadoDicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
|
||||
// first naturalize the data
|
||||
const naturalizedInstancesMetadata = data.map(naturalizeDataset);
|
||||
|
||||
const seriesSummaryMetadata = {};
|
||||
const instancesPerSeries = {};
|
||||
|
||||
naturalizedInstancesMetadata.forEach(instance => {
|
||||
if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) {
|
||||
seriesSummaryMetadata[instance.SeriesInstanceUID] = {
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
ProtocolName: instance.ProtocolName,
|
||||
Modality: instance.Modality,
|
||||
};
|
||||
}
|
||||
|
||||
if (!instancesPerSeries[instance.SeriesInstanceUID]) {
|
||||
instancesPerSeries[instance.SeriesInstanceUID] = [];
|
||||
}
|
||||
|
||||
const imageId = implementation.getImageIdsForInstance({
|
||||
instance,
|
||||
});
|
||||
|
||||
instance.imageId = imageId;
|
||||
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
});
|
||||
|
||||
instancesPerSeries[instance.SeriesInstanceUID].push(instance);
|
||||
});
|
||||
|
||||
// grab all the series metadata
|
||||
const seriesMetadata = Object.values(seriesSummaryMetadata);
|
||||
DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient);
|
||||
|
||||
Object.keys(instancesPerSeries).forEach(seriesInstanceUID =>
|
||||
DicomMetadataStore.addInstances(
|
||||
instancesPerSeries[seriesInstanceUID],
|
||||
madeInClient
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_retrieveSeriesMetadataAsync: async (
|
||||
StudyInstanceUID,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction,
|
||||
madeInClient = false
|
||||
) => {
|
||||
const enableStudyLazyLoad = true;
|
||||
// Get Series
|
||||
const {
|
||||
preLoadData: seriesSummaryMetadata,
|
||||
promises: seriesPromises,
|
||||
} = await retrieveStudyMetadata(
|
||||
wadoDicomWebClient,
|
||||
StudyInstanceUID,
|
||||
enableStudyLazyLoad,
|
||||
filters,
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
|
||||
/**
|
||||
* naturalizes the dataset, and adds a retrieve bulkdata method
|
||||
* to any values containing BulkDataURI.
|
||||
* @param {*} instance
|
||||
* @returns naturalized dataset, with retrieveBulkData methods
|
||||
*/
|
||||
const addRetrieveBulkData = instance => {
|
||||
const naturalized = naturalizeDataset(instance);
|
||||
Object.keys(naturalized).forEach(key => {
|
||||
const value = naturalized[key];
|
||||
// The value.Value will be set with the bulkdata read value
|
||||
// in which case it isn't necessary to re-read this.
|
||||
if (value && value.BulkDataURI && !value.Value) {
|
||||
// Provide a method to fetch bulkdata
|
||||
value.retrieveBulkData = () => {
|
||||
const options = {
|
||||
// The bulkdata fetches work with either multipart or
|
||||
// singlepart, so set multipart to false to let the server
|
||||
// decide which type to respond with.
|
||||
multipart: false,
|
||||
BulkDataURI: value.BulkDataURI,
|
||||
// The study instance UID is required if the bulkdata uri
|
||||
// is relative - that isn't disallowed by DICOMweb, but
|
||||
// isn't well specified in the standard, but is needed in
|
||||
// any implementation that stores static copies of the metadata
|
||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||
};
|
||||
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
||||
const ret = (val && val[0]) || undefined;
|
||||
value.Value = ret;
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
return naturalized;
|
||||
};
|
||||
|
||||
// Async load series, store as retrieved
|
||||
function storeInstances(instances) {
|
||||
const naturalizedInstances = instances.map(addRetrieveBulkData);
|
||||
|
||||
// Adding instanceMetadata to OHIF MetadataProvider
|
||||
naturalizedInstances.forEach((instance, index) => {
|
||||
const imageId = implementation.getImageIdsForInstance({
|
||||
instance,
|
||||
});
|
||||
|
||||
// Adding imageId to each instance
|
||||
// Todo: This is not the best way I can think of to let external
|
||||
// metadata handlers know about the imageId that is stored in the store
|
||||
instance.imageId = imageId;
|
||||
|
||||
// Adding UIDs to metadataProvider
|
||||
// Note: storing imageURI in metadataProvider since stack viewports
|
||||
// will use the same imageURI
|
||||
metadataProvider.addImageIdToUIDs(imageId, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
});
|
||||
});
|
||||
|
||||
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||
}
|
||||
|
||||
function setSuccessFlag() {
|
||||
const study = DicomMetadataStore.getStudy(
|
||||
StudyInstanceUID,
|
||||
madeInClient
|
||||
);
|
||||
study.isLoaded = true;
|
||||
}
|
||||
|
||||
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
|
||||
// it manually here
|
||||
seriesSummaryMetadata.forEach(aSeries => {
|
||||
aSeries.StudyInstanceUID = StudyInstanceUID;
|
||||
});
|
||||
|
||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||
|
||||
const numberOfSeries = seriesPromises.length;
|
||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
||||
const instances = await seriesPromise;
|
||||
storeInstances(instances);
|
||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||
});
|
||||
},
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet(displaySet) {
|
||||
const images = displaySet.images;
|
||||
|
||||
@@ -25,7 +25,7 @@ const seriesSortCriteria = {
|
||||
};
|
||||
|
||||
const instancesSortCriteria = {
|
||||
default: (a, b) => a.InstanceNumber - b.InstanceNumber,
|
||||
default: (a, b) => parseInt(a.InstanceNumber) - parseInt(b.InstanceNumber),
|
||||
};
|
||||
|
||||
const sortingCriteria = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
|
||||
import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
|
||||
import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
|
||||
|
||||
/**
|
||||
@@ -19,12 +19,10 @@ async function RetrieveMetadata(
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
) {
|
||||
// const RetrieveMetadataLoader =
|
||||
// enableStudyLazyLoad !== false
|
||||
// ? RetrieveMetadataLoaderAsync
|
||||
// : RetrieveMetadataLoaderSync;
|
||||
|
||||
const RetrieveMetadataLoader = RetrieveMetadataLoaderAsync;
|
||||
const RetrieveMetadataLoader =
|
||||
enableStudyLazyLoad !== false
|
||||
? RetrieveMetadataLoaderAsync
|
||||
: RetrieveMetadataLoaderSync;
|
||||
|
||||
const retrieveMetadataLoader = new RetrieveMetadataLoader(
|
||||
dicomWebClient,
|
||||
@@ -33,12 +31,9 @@ async function RetrieveMetadata(
|
||||
sortCriteria,
|
||||
sortFunction
|
||||
);
|
||||
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
|
||||
const data = await retrieveMetadataLoader.execLoad();
|
||||
|
||||
return {
|
||||
seriesSummaryMetadata: preLoadData,
|
||||
seriesPromises: promises,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
export default RetrieveMetadata;
|
||||
@@ -2,7 +2,6 @@ import dcmjs from 'dcmjs';
|
||||
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
|
||||
|
||||
/**
|
||||
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||
|
||||
@@ -60,7 +60,6 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
|
||||
}
|
||||
|
||||
async posLoad(loadData) {
|
||||
const { server } = this;
|
||||
return createStudyFromSOPInstanceList(server, loadData);
|
||||
return loadData;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
import debounce from './debounce.js';
|
||||
|
||||
export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
// commandsManager,
|
||||
}) {
|
||||
const { MeasurementService } = servicesManager.services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const debouncedSetDisplayMeasurements = debounce(
|
||||
setDisplayMeasurements,
|
||||
100
|
||||
);
|
||||
// ~~ Initial
|
||||
setDisplayMeasurements(_getMappedMeasurements(MeasurementService));
|
||||
|
||||
// ~~ Subscription
|
||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||
subscriptions.push(
|
||||
MeasurementService.subscribe(evt, () => {
|
||||
debouncedSetDisplayMeasurements(
|
||||
_getMappedMeasurements(MeasurementService)
|
||||
);
|
||||
}).unsubscribe
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, [MeasurementService]);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
amount={displayMeasurements.length}
|
||||
data={displayMeasurements}
|
||||
onClick={({ id }) => alert(`Click: ${id}`)}
|
||||
onEdit={({ id }) => alert(`Edit: ${id}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PanelMeasurementTable.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
MeasurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
function _getMappedMeasurements(MeasurementService) {
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const mappedMeasurements = measurements.map((m, index) =>
|
||||
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
|
||||
);
|
||||
|
||||
return mappedMeasurements;
|
||||
}
|
||||
|
||||
function _mapMeasurementToDisplay(measurement, index, types) {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
// Reference IDs
|
||||
referenceStudyUID,
|
||||
referenceSeriesUID,
|
||||
SOPInstanceUID,
|
||||
} = measurement;
|
||||
const instance = DicomMetadataStore.getInstance(
|
||||
referenceStudyUID,
|
||||
referenceSeriesUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
|
||||
|
||||
return {
|
||||
id: index + 1,
|
||||
label: '(empty)', // 'Label short description',
|
||||
displayText:
|
||||
_getDisplayText(
|
||||
measurement,
|
||||
PixelSpacing,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
types
|
||||
) || [],
|
||||
// TODO: handle one layer down
|
||||
isActive: false, // activeMeasurementItem === i + 1,
|
||||
};
|
||||
}
|
||||
|
||||
function _getDisplayText(
|
||||
measurement,
|
||||
pixelSpacing,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
types
|
||||
) {
|
||||
const { type, points } = measurement;
|
||||
const hasPixelSpacing =
|
||||
pixelSpacing !== undefined &&
|
||||
Array.isArray(pixelSpacing) &&
|
||||
pixelSpacing.length === 2;
|
||||
const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing
|
||||
? pixelSpacing
|
||||
: [1, 1];
|
||||
const unit = hasPixelSpacing ? 'mm' : 'px';
|
||||
|
||||
switch (type) {
|
||||
case types.POLYLINE: {
|
||||
const { length } = measurement;
|
||||
const roundedLength = _round(length, 1);
|
||||
|
||||
return [
|
||||
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
|
||||
];
|
||||
}
|
||||
case types.BIDIRECTIONAL: {
|
||||
const { shortestDiameter, longestDiameter } = measurement;
|
||||
const roundedShortestDiameter = _round(shortestDiameter, 1);
|
||||
const roundedLongestDiameter = _round(longestDiameter, 1);
|
||||
|
||||
return [
|
||||
`l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
|
||||
`s: ${roundedShortestDiameter} ${unit}`,
|
||||
];
|
||||
}
|
||||
case types.ELLIPSE: {
|
||||
const { area } = measurement;
|
||||
const roundedArea = _round(area, 1);
|
||||
|
||||
return [
|
||||
`${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`,
|
||||
];
|
||||
}
|
||||
case types.POINT: {
|
||||
const { text } = measurement;
|
||||
return [`${text} (S:${seriesNumber}, I:${instanceNumber})`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _round(value, decimals) {
|
||||
return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, ButtonGroup } from '@ohif/ui';
|
||||
|
||||
function ActionButtons({ onExportClick, onCreateReportClick }) {
|
||||
const { t } = useTranslation('MeasurementTable');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ButtonGroup color="black" size="inherit">
|
||||
<Button className="px-2 py-2 text-base" onClick={onExportClick}>
|
||||
{t('Export CSV')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{/* <Button
|
||||
className="ml-2 text-base"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color="black"
|
||||
onClick={onCreateReportClick}
|
||||
>
|
||||
{t('Create Report')}
|
||||
</Button> */}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ActionButtons.propTypes = {
|
||||
onExportClick: PropTypes.func,
|
||||
onCreateReportClick: PropTypes.func,
|
||||
};
|
||||
|
||||
ActionButtons.defaultProps = {
|
||||
onExportClick: () => alert('Export'),
|
||||
onCreateReportClick: () => alert('Create Report'),
|
||||
};
|
||||
|
||||
export default ActionButtons;
|
||||
@@ -0,0 +1,212 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MeasurementTable, Dialog, Input, useViewportGrid } from '@ohif/ui';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { downloadCSVReport } = utils;
|
||||
|
||||
// tools with measurements to display inside the panel
|
||||
const MEASUREMENT_TOOLS = [
|
||||
'EllipticalROI',
|
||||
'RectangleROI',
|
||||
'Length',
|
||||
'Bidirectional',
|
||||
];
|
||||
|
||||
export default function PanelMeasurementTable({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { MeasurementService, UIDialogService } = servicesManager.services;
|
||||
const [displayMeasurements, setDisplayMeasurements] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const debouncedSetDisplayMeasurements = debounce(
|
||||
setDisplayMeasurements,
|
||||
100
|
||||
);
|
||||
// ~~ Initial
|
||||
setDisplayMeasurements(_getMappedMeasurements(MeasurementService));
|
||||
|
||||
// ~~ Subscription
|
||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||
subscriptions.push(
|
||||
MeasurementService.subscribe(evt, () => {
|
||||
debouncedSetDisplayMeasurements(
|
||||
_getMappedMeasurements(MeasurementService)
|
||||
);
|
||||
}).unsubscribe
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
debouncedSetDisplayMeasurements.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function exportReport() {
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
|
||||
downloadCSVReport(measurements, MeasurementService);
|
||||
}
|
||||
|
||||
const jumpToImage = ({ uid, isActive }) => {
|
||||
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
|
||||
|
||||
onMeasurementItemClickHandler({ uid, isActive });
|
||||
};
|
||||
|
||||
const onMeasurementItemEditHandler = ({ uid, isActive }) => {
|
||||
const measurement = MeasurementService.getMeasurement(uid);
|
||||
//Todo: why we are jumping to image?
|
||||
// jumpToImage({ id, isActive });
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
MeasurementService.update(
|
||||
uid,
|
||||
{
|
||||
...measurement,
|
||||
...value,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
UIDialogService.dismiss({ id: 'enter-annotation' });
|
||||
};
|
||||
|
||||
UIDialogService.create({
|
||||
id: 'enter-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
noCloseButton: true,
|
||||
value: { label: measurement.label || '' },
|
||||
body: ({ value, setValue }) => {
|
||||
const onChangeHandler = event => {
|
||||
event.persist();
|
||||
setValue(value => ({ ...value, label: event.target.value }));
|
||||
};
|
||||
|
||||
const onKeyPressHandler = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmitHandler({ value, action: { id: 'save' } });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="p-4 bg-primary-dark">
|
||||
<Input
|
||||
autoFocus
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
value={value.label}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
// temp: swap button types until colors are updated
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onMeasurementItemClickHandler = ({ uid, isActive }) => {
|
||||
if (!isActive) {
|
||||
const measurements = [...displayMeasurements];
|
||||
const measurement = measurements.find(m => m.uid === uid);
|
||||
|
||||
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
|
||||
measurement.isActive = true;
|
||||
setDisplayMeasurements(measurements);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
|
||||
data-cy={'measurements-panel'}
|
||||
>
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
amount={displayMeasurements.length}
|
||||
data={displayMeasurements}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center p-4">
|
||||
<ActionButtons
|
||||
onExportClick={exportReport}
|
||||
onCreateReportClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PanelMeasurementTable.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
MeasurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
function _getMappedMeasurements(MeasurementService) {
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
// filter out measurements whose toolName is not in MEASUREMENT_TOOLS
|
||||
const measurementTools = measurements.filter(measurement =>
|
||||
MEASUREMENT_TOOLS.includes(measurement.toolName)
|
||||
);
|
||||
|
||||
const mappedMeasurements = measurementTools.map((m, index) =>
|
||||
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
|
||||
);
|
||||
|
||||
return mappedMeasurements;
|
||||
}
|
||||
|
||||
function _mapMeasurementToDisplay(measurement, index, types) {
|
||||
const { displayText, uid, label, type } = measurement;
|
||||
|
||||
return {
|
||||
uid,
|
||||
label: label || '(empty)',
|
||||
measurementType: type,
|
||||
displayText: displayText || [],
|
||||
// TODO: handle one layer down
|
||||
isActive: false, // activeMeasurementItem === i + 1,
|
||||
};
|
||||
}
|
||||
+20
-3
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyBrowser, useImageViewer } from '@ohif/ui';
|
||||
import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { formatDate } = utils;
|
||||
@@ -20,6 +20,10 @@ function PanelStudyBrowser({
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
const { StudyInstanceUIDs } = useImageViewer();
|
||||
const [
|
||||
{ activeViewportIndex, viewports },
|
||||
viewportGridService,
|
||||
] = useViewportGrid();
|
||||
const [activeTabName, setActiveTabName] = useState('primary');
|
||||
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
||||
...StudyInstanceUIDs,
|
||||
@@ -29,6 +33,13 @@ function PanelStudyBrowser({
|
||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||
});
|
||||
};
|
||||
|
||||
// ~~ studyDisplayList
|
||||
useEffect(() => {
|
||||
// Fetch all studies for the patient in each primary study
|
||||
@@ -38,6 +49,7 @@ function PanelStudyBrowser({
|
||||
|
||||
// TODO: This should be "naturalized DICOM JSON" studies
|
||||
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
||||
|
||||
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
||||
return {
|
||||
studyInstanceUid: qidoStudy.StudyInstanceUID,
|
||||
@@ -57,7 +69,7 @@ function PanelStudyBrowser({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
|
||||
|
||||
// ~~ Initial Thumbnails
|
||||
// // ~~ Initial Thumbnails
|
||||
useEffect(() => {
|
||||
const currentDisplaySets = DisplaySetService.activeDisplaySets;
|
||||
currentDisplaySets.forEach(async dSet => {
|
||||
@@ -68,7 +80,7 @@ function PanelStudyBrowser({
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
||||
if (imageId) {
|
||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||
@@ -184,10 +196,15 @@ function PanelStudyBrowser({
|
||||
}
|
||||
}
|
||||
|
||||
const activeDisplaySetInstanceUIDs =
|
||||
viewports[activeViewportIndex]?.displaySetInstanceUIDs;
|
||||
|
||||
return (
|
||||
<StudyBrowser
|
||||
tabs={tabs}
|
||||
activeTabName={activeTabName}
|
||||
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
|
||||
activeDisplaySetInstanceUIDs={activeDisplaySetInstanceUIDs}
|
||||
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
|
||||
onClickStudy={_handleStudyClick}
|
||||
onClickTab={clickedTabName => {
|
||||
+7
-8
@@ -26,7 +26,7 @@ function WrappedPanelStudyBrowser({
|
||||
dataSource
|
||||
);
|
||||
const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(
|
||||
commandsManager.getCommand.bind(commandsManager)
|
||||
extensionManager
|
||||
);
|
||||
const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
|
||||
null,
|
||||
@@ -55,14 +55,13 @@ function WrappedPanelStudyBrowser({
|
||||
* @returns {func} getImageSrcFromImageId - A utility function powered by
|
||||
* cornerstone
|
||||
*/
|
||||
function _createGetImageSrcFromImageIdFn(getCommand) {
|
||||
try {
|
||||
const command = getCommand('getCornerstoneLibraries', 'VIEWER');
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
const { cornerstone } = command.commandFn();
|
||||
function _createGetImageSrcFromImageIdFn(extensionManager) {
|
||||
const utilities = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
try {
|
||||
const { cornerstone } = utilities.exports.getCornerstoneLibraries();
|
||||
return getImageSrcFromImageId.bind(null, cornerstone);
|
||||
} catch (ex) {
|
||||
throw new Error('Required command not found');
|
||||
File renamed without changes.
@@ -4,16 +4,13 @@
|
||||
*/
|
||||
function getImageSrcFromImageId(cornerstone, imageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
cornerstone
|
||||
.loadAndCacheImage(imageId)
|
||||
.then(image => {
|
||||
const canvas = document.createElement('canvas');
|
||||
cornerstone.renderToCanvas(canvas, image);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
cornerstone.utilities
|
||||
.loadImageToCanvas(canvas, imageId)
|
||||
.then(imageId => {
|
||||
resolve(canvas.toDataURL());
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
export default getImageSrcFromImageId;
|
||||
@@ -21,6 +21,9 @@ async function getStudiesForPatientByStudyInstanceUID(
|
||||
patientId: getStudyResult[0].mrn,
|
||||
});
|
||||
}
|
||||
console.log('No mrn found for', getStudyResult);
|
||||
// The original study we KNOW belongs to the same set, so just return it
|
||||
return getStudyResult;
|
||||
}
|
||||
|
||||
export default getStudiesForPatientByStudyInstanceUID;
|
||||
@@ -1,4 +1,5 @@
|
||||
import PanelStudyBrowser from './PanelStudyBrowser';
|
||||
import WrappedPanelStudyBrowser from './WrappedPanelStudyBrowser';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable';
|
||||
|
||||
export { PanelStudyBrowser, WrappedPanelStudyBrowser };
|
||||
export { PanelStudyBrowser, WrappedPanelStudyBrowser, PanelMeasurementTable };
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+6
-6
@@ -201,11 +201,11 @@ function ViewerLayout({
|
||||
</ErrorBoundary>
|
||||
</Header>
|
||||
<div
|
||||
className="flex flex-row items-stretch w-full overflow-hidden flex-nowrap"
|
||||
className="bg-black flex flex-row items-stretch w-full overflow-hidden flex-nowrap"
|
||||
style={{ height: 'calc(100vh - 52px' }}
|
||||
>
|
||||
{/* LEFT SIDEPANELS */}
|
||||
{leftPanelComponents.length && (
|
||||
{leftPanelComponents.length ? (
|
||||
<ErrorBoundary context="Left Panel">
|
||||
<SidePanel
|
||||
side="left"
|
||||
@@ -213,10 +213,10 @@ function ViewerLayout({
|
||||
childComponents={leftPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
) : null}
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<div className="flex items-center justify-center flex-1 h-full -mt-1 overflow-hidden bg-black">
|
||||
<div className="flex items-center justify-center flex-1 h-full overflow-hidden bg-black">
|
||||
<ErrorBoundary context="Grid">
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
@@ -226,7 +226,7 @@ function ViewerLayout({
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
{rightPanelComponents.length && (
|
||||
{rightPanelComponents.length ? (
|
||||
<ErrorBoundary context="Right Panel">
|
||||
<SidePanel
|
||||
side="right"
|
||||
@@ -234,7 +234,7 @@ function ViewerLayout({
|
||||
childComponents={rightPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,25 +1,21 @@
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
ToolBarService,
|
||||
HangingProtocolService,
|
||||
CineService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
displayNotification: ({ text, title, type }) => {
|
||||
UINotificationService.show({
|
||||
title: title,
|
||||
message: text,
|
||||
type: type,
|
||||
});
|
||||
},
|
||||
clearMeasurements: () => {
|
||||
MeasurementService.clear();
|
||||
},
|
||||
toggleCine: () => {
|
||||
const { viewports } = ViewportGridService.getState();
|
||||
const { isCineEnabled } = CineService.getState();
|
||||
CineService.setIsCineEnabled(!isCineEnabled);
|
||||
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
||||
viewports.forEach((_, index) =>
|
||||
CineService.setCine({ id: index, isPlaying: false })
|
||||
);
|
||||
},
|
||||
nextStage: () => {
|
||||
// next stage in hanging protocols
|
||||
HangingProtocolService.nextProtocolStage();
|
||||
@@ -35,8 +31,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleCine: {
|
||||
commandFn: actions.toggleCine,
|
||||
displayNotification: {
|
||||
commandFn: actions.displayNotification,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
@@ -1,186 +1,61 @@
|
||||
const hangingProtocolName = 'petCT';
|
||||
|
||||
const petCTProtocol = {
|
||||
id: 'PET/CT',
|
||||
const defaultProtocol = {
|
||||
id: 'default',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'PET/CT',
|
||||
createdDate: '2021-02-23T18:32:42.849Z',
|
||||
modifiedDate: '2021-02-23T18:32:42.849Z',
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'wauZK2QNEfDPwcAQo',
|
||||
weight: 1,
|
||||
attribute: 'StudyInstanceUID',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
protocolMatchingRules: [],
|
||||
toolGroupIds: ['default'],
|
||||
stages: [
|
||||
{
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'oneByTwo',
|
||||
name: 'default',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
displaySets: [
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
windowWidth: 500,
|
||||
windowCenter: 500,
|
||||
},
|
||||
},
|
||||
commandName: '',
|
||||
// Type can be viewport or prop
|
||||
// viewport: It is most suited for settings that
|
||||
// should be applied before the first render
|
||||
// prop: It is the type of command that can be applied
|
||||
// after the image render, such as tool activations.
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
id: 'displaySet',
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'vSjk7NCYjtdS3XZAw',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "4",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
seriesMatchingRules: [],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: [
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
options: [],
|
||||
id: 'displaySet',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "1",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesNumber',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: "6",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
invert: true,
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
],
|
||||
imageMatchingRules: [
|
||||
|
||||
],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 1,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: {
|
||||
value: "Corrected",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'GPEYqFLv2dwzCM322',
|
||||
weight: 2,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
doesNotContain: {
|
||||
value: "Uncorrected",
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [
|
||||
|
||||
],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T18:32:42.850Z',
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: 0,
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: hangingProtocolName,
|
||||
protocols: [petCTProtocol],
|
||||
name: 'default',
|
||||
protocols: [defaultProtocol],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import ViewerLayout from './ViewerLayout';
|
||||
- Init layout based on the displaySets and the objects.
|
||||
*/
|
||||
|
||||
export default function ({
|
||||
export default function({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
hotkeysManager
|
||||
hotkeysManager,
|
||||
}) {
|
||||
function ViewerLayoutWithServices(props) {
|
||||
return ViewerLayout({
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import {
|
||||
InstanceMetadata,
|
||||
PhilipsPETPrivateGroup,
|
||||
} from '@cornerstonejs/calculate-suv/src/types';
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
export default function getPTImageIdInstanceMetadata(
|
||||
imageId: string
|
||||
): InstanceMetadata {
|
||||
const dicomMetaData = metadataProvider.get('instance', imageId);
|
||||
|
||||
if (!dicomMetaData) {
|
||||
throw new Error('dicom metadata are required');
|
||||
}
|
||||
|
||||
if (
|
||||
dicomMetaData.SeriesDate === undefined ||
|
||||
dicomMetaData.SeriesTime === undefined ||
|
||||
dicomMetaData.PatientWeight === undefined ||
|
||||
dicomMetaData.CorrectedImage === undefined ||
|
||||
dicomMetaData.Units === undefined ||
|
||||
!dicomMetaData.RadiopharmaceuticalInformationSequence ||
|
||||
!dicomMetaData.RadiopharmaceuticalInformationSequence.length ||
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadionuclideHalfLife === undefined ||
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadionuclideTotalDose === undefined ||
|
||||
dicomMetaData.DecayCorrection === undefined ||
|
||||
dicomMetaData.AcquisitionDate === undefined ||
|
||||
dicomMetaData.AcquisitionTime === undefined ||
|
||||
(dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadiopharmaceuticalStartDateTime === undefined &&
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadiopharmaceuticalStartTime === undefined)
|
||||
) {
|
||||
throw new Error('required metadata are missing');
|
||||
}
|
||||
|
||||
const instanceMetadata: InstanceMetadata = {
|
||||
CorrectedImage: dicomMetaData.CorrectedImage,
|
||||
Units: dicomMetaData.Units,
|
||||
RadionuclideHalfLife:
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadionuclideHalfLife,
|
||||
RadionuclideTotalDose:
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadionuclideTotalDose,
|
||||
RadiopharmaceuticalStartDateTime:
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadiopharmaceuticalStartDateTime,
|
||||
RadiopharmaceuticalStartTime:
|
||||
dicomMetaData.RadiopharmaceuticalInformationSequence[0]
|
||||
.RadiopharmaceuticalStartTime,
|
||||
DecayCorrection: dicomMetaData.DecayCorrection,
|
||||
PatientWeight: dicomMetaData.PatientWeight,
|
||||
SeriesDate: dicomMetaData.SeriesDate,
|
||||
SeriesTime: dicomMetaData.SeriesTime,
|
||||
AcquisitionDate: dicomMetaData.AcquisitionDate,
|
||||
AcquisitionTime: dicomMetaData.AcquisitionTime,
|
||||
};
|
||||
|
||||
if (
|
||||
dicomMetaData['70531000'] ||
|
||||
dicomMetaData['70531000'] !== undefined ||
|
||||
dicomMetaData['70531009'] ||
|
||||
dicomMetaData['70531009'] !== undefined
|
||||
) {
|
||||
const philipsPETPrivateGroup: PhilipsPETPrivateGroup = {
|
||||
SUVScaleFactor: dicomMetaData['70531000'],
|
||||
ActivityConcentrationScaleFactor: dicomMetaData['70531009'],
|
||||
};
|
||||
instanceMetadata.PhilipsPETPrivateGroup = philipsPETPrivateGroup;
|
||||
}
|
||||
|
||||
if (dicomMetaData['0009100d'] && dicomMetaData['0009100d'] !== undefined) {
|
||||
instanceMetadata.GEPrivatePostInjectionDateTime = dicomMetaData['0009100d'];
|
||||
}
|
||||
|
||||
if (
|
||||
dicomMetaData.FrameReferenceTime &&
|
||||
dicomMetaData.FrameReferenceTime !== undefined
|
||||
) {
|
||||
instanceMetadata.FrameReferenceTime = dicomMetaData.FrameReferenceTime;
|
||||
}
|
||||
|
||||
if (
|
||||
dicomMetaData.ActualFrameDuration &&
|
||||
dicomMetaData.ActualFrameDuration !== undefined
|
||||
) {
|
||||
instanceMetadata.ActualFrameDuration = dicomMetaData.ActualFrameDuration;
|
||||
}
|
||||
|
||||
if (dicomMetaData.PatientSex && dicomMetaData.PatientSex !== undefined) {
|
||||
instanceMetadata.PatientSex = dicomMetaData.PatientSex;
|
||||
}
|
||||
|
||||
if (dicomMetaData.PatientSize && dicomMetaData.PatientSize !== undefined) {
|
||||
instanceMetadata.PatientSize = dicomMetaData.PatientSize;
|
||||
}
|
||||
|
||||
return instanceMetadata;
|
||||
}
|
||||
|
||||
function convertInterfaceTimeToString(time): string {
|
||||
const hours = `${time.hours || '00'}`.padStart(2, '0');
|
||||
const minutes = `${time.minutes || '00'}`.padStart(2, '0');
|
||||
const seconds = `${time.seconds || '00'}`.padStart(2, '0');
|
||||
|
||||
const fractionalSeconds = `${time.fractionalSeconds || '000000'}`.padEnd(
|
||||
6,
|
||||
'0'
|
||||
);
|
||||
|
||||
const timeString = `${hours}${minutes}${seconds}.${fractionalSeconds}`;
|
||||
return timeString;
|
||||
}
|
||||
|
||||
function convertInterfaceDateToString(date): string {
|
||||
const month = `${date.month}`.padStart(2, '0');
|
||||
const day = `${date.day}`.padStart(2, '0');
|
||||
const dateString = `${date.year}${month}${day}`;
|
||||
return dateString;
|
||||
}
|
||||
|
||||
export { getPTImageIdInstanceMetadata };
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WrappedPanelStudyBrowser } from './Panels';
|
||||
|
||||
import PanelMeasurementTable from './PanelMeasurementTable.js';
|
||||
import { WrappedPanelStudyBrowser, PanelMeasurementTable } from './Panels';
|
||||
|
||||
// TODO:
|
||||
// - No loading UI exists yet
|
||||
@@ -13,6 +13,8 @@ const makeDisplaySet = instances => {
|
||||
const instance = instances[0];
|
||||
const imageSet = new ImageSet(instances);
|
||||
|
||||
const displayReconstructableInfo = isDisplaySetReconstructable(instances);
|
||||
|
||||
// set appropriate attributes to image set...
|
||||
imageSet.setAttributes({
|
||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||
@@ -27,6 +29,7 @@ const makeDisplaySet = instances => {
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
numImageFrames: instances.length,
|
||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||
isReconstructable: displayReconstructableInfo.value,
|
||||
});
|
||||
|
||||
// Sort the images in this series if needed
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider.jsx';
|
||||
import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.jsx';
|
||||
import ToolbarSplitButton from './Toolbar/ToolbarSplitButton.jsx';
|
||||
import ToolbarDivider from './Toolbar/ToolbarDivider.tsx';
|
||||
import ToolbarLayoutSelector from './Toolbar/ToolbarLayoutSelector.tsx';
|
||||
import ToolbarSplitButton from './Toolbar/ToolbarSplitButton.tsx';
|
||||
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
const toolbarService = servicesManager.services.ToolBarService;
|
||||
@@ -1,17 +1,22 @@
|
||||
import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
import getPanelModule from './getPanelModule.js';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
||||
import getToolbarModule from './getToolbarModule.js';
|
||||
import getToolbarModule from './getToolbarModule';
|
||||
import commandsModule from './commandsModule';
|
||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||
import { id } from './id.js';
|
||||
import init from './init';
|
||||
|
||||
export default {
|
||||
const defaultExtension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
preRegistration: ({ servicesManager, configuration = {} }) => {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getDataSourcesModule,
|
||||
getHangingProtocolModule,
|
||||
getLayoutTemplateModule,
|
||||
@@ -21,4 +26,16 @@ export default {
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return commandsModule({ servicesManager, commandsManager });
|
||||
},
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'common',
|
||||
exports: {
|
||||
getStudiesForPatientByStudyInstanceUID,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultExtension;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { DicomMetadataStore, classes } from '@ohif/core';
|
||||
import { calculateSUVScalingFactors } from '@cornerstonejs/calculate-suv';
|
||||
|
||||
import getPTImageIdInstanceMetadata from './getPTImageIdInstanceMetadata';
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
handlePETImageMetadata
|
||||
);
|
||||
|
||||
// If the metadata for PET has changed by the user (e.g. manually changing the PatientWeight)
|
||||
// we need to recalculate the SUV Scaling Factors
|
||||
DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.SERIES_UPDATED,
|
||||
handlePETImageMetadata
|
||||
);
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
const { instances } = DicomMetadataStore.getSeries(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
const modality = instances[0].Modality;
|
||||
if (modality !== 'PT') {
|
||||
return;
|
||||
}
|
||||
const imageIds = instances.map(instance => instance.imageId);
|
||||
const InstanceMetadataArray = [];
|
||||
imageIds.forEach(imageId => {
|
||||
const instanceMetadata = getPTImageIdInstanceMetadata(imageId);
|
||||
if (instanceMetadata) {
|
||||
InstanceMetadataArray.push(instanceMetadata);
|
||||
}
|
||||
});
|
||||
|
||||
if (!InstanceMetadataArray.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// try except block to prevent errors when the metadata is not correct
|
||||
let suvScalingFactors;
|
||||
try {
|
||||
suvScalingFactors = calculateSUVScalingFactors(InstanceMetadataArray);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (!suvScalingFactors) {
|
||||
return;
|
||||
}
|
||||
|
||||
InstanceMetadataArray.forEach((instanceMetadata, index) => {
|
||||
metadataProvider.addCustomMetadata(
|
||||
imageIds[index],
|
||||
'scalingModule',
|
||||
suvScalingFactors[index]
|
||||
);
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user