diff --git a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js
index 9988544..e780fc6 100644
--- a/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js
+++ b/extensions/ohif-gradienthealth-extension/src/DicomJSONDataSource/index.js
@@ -53,6 +53,12 @@ const getInstanceUrl = (url, prefix) => {
return modifiedUrl;
}
+const getProperty = (serieMetadata, property) => {
+ return (
+ serieMetadata[property] || serieMetadata.instances[0].metadata[property]
+ );
+};
+
const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
// TODO: bq should not have dups
let filteredRows = rows.map(row => {
@@ -87,6 +93,7 @@ const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
SeriesDescription: row['SeriesDescription'] || 'No description',
StudyInstanceUID: row['StudyInstanceUID'],
SeriesNumber: row['SeriesNumber'],
+ SeriesDate: row['SeriesDate'],
SeriesTime: row['SeriesTime'],
NumInstances: isNaN(parseInt(row['NumInstances']))
? 0
@@ -101,16 +108,17 @@ const getMetadataFromRows = (rows, prefix, seriesuidArray) => {
});
return {
- StudyInstanceUID: rows[0]['StudyInstanceUID'],
- PatientName: rows[0]['PatientName'],
- PatientSex: rows[0]['PatientSex'],
- AccessionNumber: rows[0]['AccessionNumber'],
- StudyDate: rows[0]['StudyDate'],
- PatientID: rows[0]['PatientID'],
- PatientWeight: rows[0]['PatientWeight'],
- PatientAge: rows[0]['PatientAge'],
- StudyDescription: rows[0]['StudyDescription'] || 'No description',
- StudyTime: rows[0]['StudyTime'],
+ StudyInstanceUID: getProperty(rows[0], 'StudyInstanceUID'),
+ PatientName: getProperty(rows[0], 'PatientName'),
+ PatientSex: getProperty(rows[0], 'PatientSex') || '',
+ AccessionNumber: getProperty(rows[0], 'AccessionNumber'),
+ StudyDate: getProperty(rows[0], 'StudyDate'),
+ PatientID: getProperty(rows[0], 'PatientID'),
+ PatientWeight: getProperty(rows[0], 'PatientWeight') || '',
+ PatientAge: getProperty(rows[0], 'PatientAge') || '',
+ StudyDescription:
+ getProperty(rows[0], 'StudyDescription') || 'No description',
+ StudyTime: getProperty(rows[0], 'StudyTime'),
NumInstances: studyNumInstances,
Modalities: `["${rows[0]['Modality']}"]`,
series: series,
@@ -179,7 +187,7 @@ const getBigQueryRows = async (studyuids, seriesuid, access_token) => {
const filesFromStudyInstanceUID = async ({bucketName, prefix, studyuids, headers})=>{
const studyMetadata = studyuids.map(async (studyuid) => {
- const folderPath = `${prefix}/${studyuid}/`;
+ const folderPath = `${prefix}/studies/${studyuid}/series/`;
const delimiter = '/'
const apiUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}/o?prefix=${folderPath}&delimiter=${delimiter}`;
const response = await fetch(apiUrl, { headers });
@@ -187,7 +195,7 @@ const filesFromStudyInstanceUID = async ({bucketName, prefix, studyuids, headers
const files = res.items || [];
const folders = res.prefixes || [];
const series = folders.map(async (folderPath)=>{
- const objectName = `${folderPath}metadata.json.gz`;
+ const objectName = `${folderPath}metadata`;
const apiUrl = `https://storage.googleapis.com/storage/v1/b/${bucketName}/o/${encodeURIComponent(objectName)}?alt=media`;
const response = await fetch(apiUrl, { headers });
return response.json()
@@ -216,7 +224,16 @@ const mapSegSeriesFromDataSet = (dataSet) => {
SeriesDescription: dataSet.SeriesDescription,
SeriesNumber: Number(dataSet.SeriesNumber),
SeriesDate: dataSet.SeriesDate,
+ SeriesTime: dataSet.SeriesTime,
+ StudyDate: dataSet.StudyDate,
+ StudyTime: dataSet.StudyTime,
+ PatientName: dataSet.PatientName,
+ PatientID: dataSet.PatientID,
+ PatientWeight: dataSet.PatientWeight,
+ PatientAge: Number(dataSet.PatientAge),
+ AccessionNumber: Number(dataSet.AccessionNumber),
StudyInstanceUID: dataSet.StudyInstanceUID,
+ StudyDescription: dataSet.StudyDescription,
instances: [
{
metadata: {
@@ -224,15 +241,20 @@ const mapSegSeriesFromDataSet = (dataSet) => {
SOPInstanceUID: dataSet.SOPInstanceUID,
SOPClassUID: dataSet.SOPClassUID,
ReferencedSeriesSequence: dataSet.ReferencedSeriesSequence,
- SharedFunctionalGroupsSequence: dataSet.SharedFunctionalGroupsSequence,
+ SharedFunctionalGroupsSequence:
+ dataSet.SharedFunctionalGroupsSequence,
+ PerFrameFunctionalGroupsSequence:
+ dataSet.PerFrameFunctionalGroupsSequence,
+ SegmentSequence: dataSet.SegmentSequence,
+ Manufacturer: dataSet.Manufacturer,
},
url: dataSet.url,
- }
+ },
],
};
};
-const storeDicomSeg = async (naturalizedReport, headers) => {
+const storeDicomSeg = async (naturalizedReport, headers, displaySetService) => {
const {
StudyInstanceUID,
SeriesInstanceUID,
@@ -241,16 +263,32 @@ const storeDicomSeg = async (naturalizedReport, headers) => {
} = naturalizedReport;
const params = new URLSearchParams(window.location.search);
- const bucket = params.get('bucket') || 'gradient-health-search-viewer-links';
+ const buckets = params.getAll('bucket');
+ const bucket =
+ buckets[1] || buckets[0] || 'gradient-health-search-viewer-links';
const prefix = params.get('bucket-prefix') || 'dicomweb';
- const segBucket = params.get('seg-bucket') || bucket
+ let segBucket = params.get('seg-bucket') || bucket
const segPrefix = params.get('seg-prefix') || prefix
+ const filteredDescription = SeriesDescription.replace(/[/]/g, '');
- const fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
- SeriesDescription
+ let fileName = `${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/${encodeURIComponent(
+ filteredDescription
)}.dcm`;
- const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}`;
+
+ const segDisplaySet = displaySetService.getDisplaySetsBy(
+ (ds) =>
+ ds.SeriesInstanceUID === SeriesInstanceUID &&
+ ds.instance.SOPInstanceUID === SOPInstanceUID
+ )[0];
+ if (segDisplaySet) {
+ const url = segDisplaySet.instance.url;
+ segBucket = url.split('https://storage.googleapis.com/')[1].split('/')[0];
+ fileName = url.split(`https://storage.googleapis.com/${segBucket}/`)[1];
+ }
+
+ const segUploadUri = `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${fileName}&contentEncoding=gzip`;
const blob = datasetToBlob(naturalizedReport);
+ const compressedFile = pako.gzip(await blob.arrayBuffer());
await fetch(segUploadUri, {
method: 'POST',
@@ -258,7 +296,7 @@ const storeDicomSeg = async (naturalizedReport, headers) => {
...headers,
'Content-Type': 'application/dicom',
},
- body: blob,
+ body: compressedFile,
})
.then((response) => response.json())
.then((data) => {
@@ -275,7 +313,7 @@ const storeDicomSeg = async (naturalizedReport, headers) => {
const compressedFile = pako.gzip(JSON.stringify(segSeries));
return fetch(
- `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${segPrefix}/${StudyInstanceUID}/${SeriesInstanceUID}/metadata.json.gz&contentEncoding=gzip`,
+ `https://storage.googleapis.com/upload/storage/v1/b/${segBucket}/o?uploadType=media&name=${segPrefix}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/metadata&contentEncoding=gzip`,
{
method: 'POST',
headers: {
@@ -320,6 +358,10 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
},
initialize: async ({ params, query, url }) => {
if (!url) url = query.get('url');
+ if (!url) {
+ url = query.toString();
+ query.set('url', url);
+ }
let metaData = getMetaDataByURL(url);
// if we have already cached the data from this specific url
@@ -331,19 +373,29 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
});
}
+ const buckets = query.getAll('bucket');
+ if (buckets.length === 0)
+ buckets.push('gradient-health-search-viewer-links');
+
const { UserAuthenticationService } = servicesManager.services;
- const studyMetadata = await filesFromStudyInstanceUID({
- bucketName: query.get('bucket') || 'gradient-health-search-viewer-links',
- prefix: query.get('bucket-prefix') || 'dicomweb',
- studyuids: query.getAll('StudyInstanceUID'),
- headers: UserAuthenticationService.getAuthorizationHeader()
- });
+
+ const studyMetadata = [];
+ for (let i = 0; i < buckets.length; i++) {
+ const metadataPerBucket = await filesFromStudyInstanceUID({
+ bucketName: buckets[i],
+ prefix: query.get('bucket-prefix') || 'dicomweb',
+ studyuids: query.getAll('StudyInstanceUID'),
+ headers: UserAuthenticationService.getAuthorizationHeader(),
+ });
+
+ studyMetadata.push(...metadataPerBucket);
+ }
const data = getMetadataFromRows(
- _.flatten(studyMetadata),
- query.get('prefix'),
+ _.flatten(studyMetadata),
+ query.get('prefix'),
query.getAll('SeriesInstanceUID')
- );
+ );
let StudyInstanceUID;
let SeriesInstanceUID;
@@ -422,8 +474,9 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
console.debug('Not implemented', params)
},
series: {
- metadata: ({
+ metadata: async ({
StudyInstanceUID,
+ buckets = [],
madeInClient = false,
customSort,
} = {}) => {
@@ -433,7 +486,22 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
);
}
- const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
+ let study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
+
+ if (!study) {
+ // If the study is not found, initialize the study.
+ // If there is no buckets in the url default bucket will be used.
+ const params = new URLSearchParams(window.location.search);
+ params.set('StudyInstanceUID', StudyInstanceUID);
+ params.delete('bucket');
+ buckets.forEach((bucket) => {
+ params.append('bucket', bucket);
+ });
+ await implementation.initialize({ query: params });
+
+ study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
+ }
+
let series;
if (customSort) {
@@ -494,7 +562,11 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
if (dataset.Modality === 'SEG') {
const headers = servicesManager.services.UserAuthenticationService.getAuthorizationHeader()
try {
- await storeDicomSeg(dataset, headers)
+ await storeDicomSeg(
+ dataset,
+ headers,
+ servicesManager.services.displaySetService
+ );
} catch (error) {
throw error
}
diff --git a/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/DisplayValue.tsx b/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/DisplayValue.tsx
index 9b37970..4decff8 100644
--- a/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/DisplayValue.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/DisplayValue.tsx
@@ -8,7 +8,7 @@ export default function DisplayValue({formIndex, name, value, defaultValue, opti
{ name }:
-
+
{ value !== null ? value.toString() : defaultValue !== null ? defaultValue.toString() : '' }
diff --git a/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/Textarea.tsx b/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/Textarea.tsx
index b324b6a..5309b99 100644
--- a/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/Textarea.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/_shared/FormGeneratorComponent/fields/Textarea.tsx
@@ -1,18 +1,21 @@
-import * as React from 'react';
-import { useMemo } from 'react';
+import React, { useState, useMemo, useEffect } from 'react';
import Paper from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import debounce from 'lodash.debounce';
export default function Textarea({formIndex, name, value, defaultValue, options, onChange}) {
- const [val, setVal] = React.useState(value !== null ? value : (defaultValue !== null ? defaultValue : ''));
+ const [val, setVal] = useState(value ?? defaultValue ?? '');
const { rows } = options
const debouncedOnChange = useMemo(
() => debounce((formIndex, value) => {
- onChange({formIndex, value})
- }, 600), [onChange]
+ onChange({formIndex, value})
+ }, 600), [onChange]
);
+ useEffect(() => {
+ setVal(value ?? defaultValue ?? '');
+ }, [value, defaultValue]);
+
const handleChange = (event: React.ChangeEvent) => {
setVal(event.target.value);
debouncedOnChange(formIndex, event.target.value)
diff --git a/extensions/ohif-gradienthealth-extension/src/getPanelModule.tsx b/extensions/ohif-gradienthealth-extension/src/getPanelModule.tsx
index f25619e..4fce9e3 100644
--- a/extensions/ohif-gradienthealth-extension/src/getPanelModule.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/getPanelModule.tsx
@@ -2,7 +2,8 @@ import {
PanelMeasurementTableTracking,
PanelStudyBrowserTracking,
PanelForm,
- PanelFormAndMeasurementTable
+ PanelFormAndMeasurementTable,
+ PanelStudyBrowser,
} from './panels';
// TODO:
@@ -59,6 +60,17 @@ function getPanelModule({
servicesManager,
}),
},
+ {
+ name: 'seriesList-without-tracking',
+ iconName: 'group-layers',
+ iconLabel: 'Studies',
+ label: 'Studies',
+ component: PanelStudyBrowser.bind(null, {
+ commandsManager,
+ extensionManager,
+ servicesManager,
+ }),
+ },
];
}
diff --git a/extensions/ohif-gradienthealth-extension/src/index.tsx b/extensions/ohif-gradienthealth-extension/src/index.tsx
index 1f2015d..ed01167 100644
--- a/extensions/ohif-gradienthealth-extension/src/index.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/index.tsx
@@ -7,6 +7,7 @@ import { id } from './id.js';
import GoogleSheetsService from './services/GoogleSheetsService';
import CropDisplayAreaService from './services/CropDisplayAreaService';
import CacheAPIService from './services/CacheAPIService';
+import addSegmentationLabelModifier from './utils/addSegmentationLabelModifier';
// import { CornerstoneEventTarget } from '@cornerstonejs/core/CornerstoneEventTarget';
// import { Events } from '@cornerstonejs/core/Events';
@@ -16,6 +17,9 @@ const gradientHealthExtension = {
* Only required property. Should be a unique value across all extensions.
*/
id,
+ onModeEnter({ servicesManager}){
+ addSegmentationLabelModifier(servicesManager)
+ },
getDataSourcesModule: ({ servicesManager }) => {
return getDataSourcesModule({ servicesManager });
},
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelForm/index.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelForm/index.tsx
index f18b621..353a136 100644
--- a/extensions/ohif-gradienthealth-extension/src/panels/PanelForm/index.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelForm/index.tsx
@@ -53,7 +53,7 @@ function PanelForm({ servicesManager, extensionManager }) {
useEffect(() => {
if(!firstLoad){
setLoading(true)
- GoogleSheetsService.writeFormToRow(formValue).then((values)=>{
+ GoogleSheetsService.updateRow(formValue).then((values)=>{
setLoading(false)
})
}
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/PanelStudyBrowser.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/PanelStudyBrowser.tsx
new file mode 100644
index 0000000..7ba9aa7
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/PanelStudyBrowser.tsx
@@ -0,0 +1,407 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui';
+import { utils } from '@ohif/core';
+import { useNavigate } from 'react-router-dom';
+
+const { sortStudyInstances, formatDate } = utils;
+
+/**
+ *
+ * @param {*} param0
+ */
+function PanelStudyBrowser({
+ servicesManager,
+ getImageSrc,
+ getStudiesForPatientByMRN,
+ requestDisplaySetCreationForStudy,
+ dataSource,
+}) {
+ const { hangingProtocolService, displaySetService, uiNotificationService, GoogleSheetsService } =
+ servicesManager.services;
+ const navigate = useNavigate();
+
+ // Normally you nest the components so the tree isn't so deep, and the data
+ // doesn't have to have such an intense shape. This works well enough for now.
+ // Tabs --> Studies --> DisplaySets --> Thumbnails
+ const { StudyInstanceUIDs } = useImageViewer();
+ const [studyInstanceUIDs, setStudyInstanceUIDs] = useState([...StudyInstanceUIDs]);
+ const [{ activeViewportId, viewports }, viewportGridService] = useViewportGrid();
+ const [activeTabName, setActiveTabName] = useState('primary');
+ const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
+ ...studyInstanceUIDs,
+ ]);
+ const [studyDisplayList, setStudyDisplayList] = useState([]);
+ const [displaySets, setDisplaySets] = useState([]);
+ const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
+
+ const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
+ let updatedViewports = [];
+ const viewportId = activeViewportId;
+ try {
+ updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
+ viewportId,
+ displaySetInstanceUID
+ );
+ } catch (error) {
+ console.warn(error);
+ uiNotificationService.show({
+ title: 'Thumbnail Double Click',
+ message: 'The selected display sets could not be added to the viewport.',
+ type: 'info',
+ duration: 3000,
+ });
+ }
+
+ viewportGridService.setDisplaySetsForViewports(updatedViewports);
+ };
+
+ useEffect(() => {
+ setStudyInstanceUIDs([...StudyInstanceUIDs]);
+ setExpandedStudyInstanceUIDs([...StudyInstanceUIDs]);
+ }, [StudyInstanceUIDs]);
+
+ // ~~ studyDisplayList
+ useEffect(() => {
+ // Fetch all studies for the patient in each primary study
+ async function fetchStudiesForPatient(StudyInstanceUID) {
+ // current study qido
+ const qidoForStudyUID = await dataSource.query.studies.search({
+ studyInstanceUid: StudyInstanceUID,
+ });
+
+ if (!qidoForStudyUID?.length) {
+ navigate('/notfoundstudy', '_self');
+ throw new Error('Invalid study URL');
+ }
+
+ let qidoStudiesForPatient = qidoForStudyUID;
+
+ // try to fetch the prior studies based on the patientID if the
+ // server can respond.
+ try {
+ qidoStudiesForPatient = await getStudiesForPatientByMRN(qidoForStudyUID);
+ } catch (error) {
+ console.warn(error);
+ }
+
+ const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
+ const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
+ return {
+ studyInstanceUid: qidoStudy.StudyInstanceUID,
+ date: formatDate(qidoStudy.StudyDate),
+ description: qidoStudy.StudyDescription,
+ modalities: qidoStudy.ModalitiesInStudy,
+ numInstances: qidoStudy.NumInstances,
+ };
+ });
+
+ setStudyDisplayList(prevArray => {
+ const ret = [...prevArray];
+ for (const study of actuallyMappedStudies) {
+ if (!prevArray.find(it => it.studyInstanceUid === study.studyInstanceUid)) {
+ ret.push(study);
+ }
+ }
+ return ret;
+ });
+ }
+
+ studyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
+ }, [studyInstanceUIDs, dataSource, getStudiesForPatientByMRN, navigate]);
+
+ // // ~~ Initial Thumbnails
+ useEffect(() => {
+ const currentDisplaySets = displaySetService.activeDisplaySets;
+ currentDisplaySets.forEach(async dSet => {
+ const newImageSrcEntry = {};
+ const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
+ 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?
+ if (!imageId || displaySet?.unsupported) {
+ return;
+ }
+ // When the image arrives, render it and store the result in the thumbnailImgSrcMap
+ newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
+
+ setThumbnailImageSrcMap(prevState => {
+ return { ...prevState, ...newImageSrcEntry };
+ });
+ });
+ }, [studyInstanceUIDs, dataSource, displaySetService, getImageSrc]);
+
+ // ~~ displaySets
+ useEffect(() => {
+ // TODO: Are we sure `activeDisplaySets` will always be accurate?
+ const currentDisplaySets = displaySetService.activeDisplaySets;
+ const mappedDisplaySets = _mapDisplaySets(currentDisplaySets, thumbnailImageSrcMap);
+ sortStudyInstances(mappedDisplaySets);
+
+ setDisplaySets(mappedDisplaySets);
+ }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
+
+ // ~~ subscriptions --> displaySets
+ useEffect(() => {
+ // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
+ const SubscriptionDisplaySetsAdded = displaySetService.subscribe(
+ displaySetService.EVENTS.DISPLAY_SETS_ADDED,
+ data => {
+ const { displaySetsAdded, options } = data;
+ displaySetsAdded.forEach(async dSet => {
+ const newImageSrcEntry = {};
+ const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
+ if (displaySet?.unsupported) {
+ return;
+ }
+
+ 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?
+ if (!imageId) {
+ return;
+ }
+ // When the image arrives, render it and store the result in the thumbnailImgSrcMap
+ newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
+ imageId,
+ dSet.initialViewport
+ );
+
+ setThumbnailImageSrcMap(prevState => {
+ return { ...prevState, ...newImageSrcEntry };
+ });
+ });
+ }
+ );
+
+ return () => {
+ SubscriptionDisplaySetsAdded.unsubscribe();
+ };
+ }, [getImageSrc, dataSource, displaySetService]);
+
+ useEffect(() => {
+ // TODO: Will this always hold _all_ the displaySets we care about?
+ // DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
+ const SubscriptionDisplaySetsChanged = displaySetService.subscribe(
+ displaySetService.EVENTS.DISPLAY_SETS_CHANGED,
+ changedDisplaySets => {
+ const mappedDisplaySets = _mapDisplaySets(changedDisplaySets, thumbnailImageSrcMap);
+ setDisplaySets(mappedDisplaySets);
+ }
+ );
+
+ const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe(
+ displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
+ () => {
+ const mappedDisplaySets = _mapDisplaySets(
+ displaySetService.getActiveDisplaySets(),
+ thumbnailImageSrcMap
+ );
+
+ setDisplaySets(mappedDisplaySets);
+ }
+ );
+
+ return () => {
+ SubscriptionDisplaySetsChanged.unsubscribe();
+ SubscriptionDisplaySetMetaDataInvalidated.unsubscribe();
+ };
+ }, [studyInstanceUIDs, thumbnailImageSrcMap, displaySetService]);
+
+ useEffect(() => {
+ const { unsubscribe } = GoogleSheetsService.subscribe(
+ GoogleSheetsService.EVENTS.GOOGLE_SHEETS_CHANGE,
+ () => {
+ const newStudyInstanceUID = Object.entries(GoogleSheetsService.studyUIDToIndex).filter(
+ ([key, val]) => val === GoogleSheetsService.index
+ )[0][0];
+
+ if (!studyInstanceUIDs.includes(newStudyInstanceUID)) {
+ setStudyInstanceUIDs([newStudyInstanceUID]);
+ setExpandedStudyInstanceUIDs([newStudyInstanceUID]);
+ }
+ }
+ );
+
+ return () => {
+ unsubscribe();
+ };
+ });
+
+ const tabs = _createStudyBrowserTabs(studyInstanceUIDs, studyDisplayList, displaySets);
+
+ // TODO: Should not fire this on "close"
+ function _handleStudyClick(StudyInstanceUID) {
+ const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(StudyInstanceUID);
+ const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
+ ? // eslint-disable-next-line prettier/prettier
+ [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
+ : [...expandedStudyInstanceUIDs, StudyInstanceUID];
+
+ setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
+
+ if (!shouldCollapseStudy) {
+ const madeInClient = true;
+ requestDisplaySetCreationForStudy(displaySetService, StudyInstanceUID, madeInClient);
+ }
+ }
+
+ const activeDisplaySetInstanceUIDs = viewports.get(activeViewportId)?.displaySetInstanceUIDs;
+
+ return (
+ {
+ setActiveTabName(clickedTabName);
+ }}
+ />
+ );
+}
+
+PanelStudyBrowser.propTypes = {
+ servicesManager: PropTypes.object.isRequired,
+ dataSource: PropTypes.shape({
+ getImageIdsForDisplaySet: PropTypes.func.isRequired,
+ }).isRequired,
+ getImageSrc: PropTypes.func.isRequired,
+ getStudiesForPatientByMRN: PropTypes.func.isRequired,
+ requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
+};
+
+export default PanelStudyBrowser;
+
+/**
+ * Maps from the DataSource's format to a naturalized object
+ *
+ * @param {*} studies
+ */
+function _mapDataSourceStudies(studies) {
+ return studies.map(study => {
+ // TODO: Why does the data source return in this format?
+ return {
+ AccessionNumber: study.accession,
+ StudyDate: study.date,
+ StudyDescription: study.description,
+ NumInstances: study.instances,
+ ModalitiesInStudy: study.modalities,
+ PatientID: study.mrn,
+ PatientName: study.patientName,
+ StudyInstanceUID: study.studyInstanceUid,
+ StudyTime: study.time,
+ };
+ });
+}
+
+function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
+ const thumbnailDisplaySets: any[] = [];
+ const thumbnailNoImageDisplaySets: any[] = [];
+
+ displaySets
+ .filter((ds) => !ds.excludeFromThumbnailBrowser)
+ .forEach((ds) => {
+ const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
+ const componentType = _getComponentType(ds);
+
+ const array =
+ componentType === 'thumbnail'
+ ? thumbnailDisplaySets
+ : thumbnailNoImageDisplaySets;
+
+ array.push({
+ displaySetInstanceUID: ds.displaySetInstanceUID,
+ description: ds.SeriesDescription || '',
+ seriesNumber: ds.SeriesNumber,
+ modality: ds.Modality,
+ seriesDate: ds.SeriesDate,
+ seriesTime: ds.SeriesTime,
+ numInstances: ds.numImageFrames,
+ countIcon: ds.countIcon,
+ StudyInstanceUID: ds.StudyInstanceUID,
+ messages: ds.messages,
+ componentType,
+ imageSrc,
+ dragData: {
+ type: 'displayset',
+ displaySetInstanceUID: ds.displaySetInstanceUID,
+ // .. Any other data to pass
+ },
+ isHydratedForDerivedDisplaySet: ds.isHydrated,
+ });
+ });
+
+ return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets];
+}
+
+const thumbnailNoImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
+
+function _getComponentType(ds) {
+ if (thumbnailNoImageModalities.includes(ds.Modality) || ds?.unsupported) {
+ // TODO probably others.
+ return 'thumbnailNoImage';
+ }
+
+ return 'thumbnail';
+}
+
+/**
+ *
+ * @param {string[]} primaryStudyInstanceUIDs
+ * @param {object[]} studyDisplayList
+ * @param {string} studyDisplayList.studyInstanceUid
+ * @param {string} studyDisplayList.date
+ * @param {string} studyDisplayList.description
+ * @param {string} studyDisplayList.modalities
+ * @param {number} studyDisplayList.numInstances
+ * @param {object[]} displaySets
+ * @returns tabs - The prop object expected by the StudyBrowser component
+ */
+function _createStudyBrowserTabs(primaryStudyInstanceUIDs, studyDisplayList, displaySets) {
+ const primaryStudies = [];
+ const recentStudies = [];
+ const allStudies = [];
+
+ studyDisplayList.forEach(study => {
+ const displaySetsForStudy = displaySets.filter(
+ ds => ds.StudyInstanceUID === study.studyInstanceUid
+ );
+ const tabStudy = Object.assign({}, study, {
+ displaySets: displaySetsForStudy,
+ });
+
+ if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) {
+ primaryStudies.push(tabStudy);
+ } else {
+ // TODO: Filter allStudies to dates within one year of current date
+ recentStudies.push(tabStudy);
+ allStudies.push(tabStudy);
+ }
+ });
+
+ const tabs = [
+ {
+ name: 'primary',
+ label: 'Primary',
+ studies: primaryStudies,
+ },
+ {
+ name: 'recent',
+ label: 'Recent',
+ studies: recentStudies,
+ },
+ {
+ name: 'all',
+ label: 'All',
+ studies: allStudies,
+ },
+ ];
+
+ return tabs;
+}
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/index.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/index.tsx
new file mode 100644
index 0000000..9625122
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowser/index.tsx
@@ -0,0 +1,56 @@
+import React, { useCallback } from 'react';
+import PropTypes from 'prop-types';
+
+import PanelStudyBrowser from './PanelStudyBrowser';
+import { studyPanelUtilities } from '../utils';
+
+const {
+ createGetImageSrcFromImageIdFn,
+ createRequestDisplaySetcreationFn,
+ getStudyForPatientUtility,
+} = studyPanelUtilities;
+
+/**
+ * Wraps the PanelStudyBrowser and provides features afforded by managers/services
+ *
+ * @param {object} params
+ * @param {object} commandsManager
+ * @param {object} extensionManager
+ */
+function WrappedPanelStudyBrowser({
+ commandsManager,
+ extensionManager,
+ servicesManager,
+}) {
+ // TODO: This should be made available a different way; route should have
+ // already determined our datasource
+ const dataSource = extensionManager.getDataSources()[0];
+ const _getStudiesForPatientByMRN = getStudyForPatientUtility(
+ extensionManager,
+ dataSource
+ );
+ const _getImageSrcFromImageId = useCallback(
+ createGetImageSrcFromImageIdFn(extensionManager),
+ []
+ );
+ const _requestDisplaySetCreationForStudy =
+ createRequestDisplaySetcreationFn(dataSource);
+
+ return (
+
+ );
+}
+
+WrappedPanelStudyBrowser.propTypes = {
+ commandsManager: PropTypes.object.isRequired,
+ extensionManager: PropTypes.object.isRequired,
+ servicesManager: PropTypes.object.isRequired,
+};
+
+export default WrappedPanelStudyBrowser;
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx
index df1e067..b480f53 100644
--- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx
+++ b/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/index.tsx
@@ -1,18 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
-//
-import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
-import getImageSrcFromImageId from './getImageSrcFromImageId';
-import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
-function _getStudyForPatientUtility(extensionManager) {
- const utilityModule = extensionManager.getModuleEntry(
- '@ohif/extension-default.utilityModule.common'
- );
+import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
+import { studyPanelUtilities } from '../utils';
- const { getStudiesForPatientByMRN } = utilityModule.exports;
- return getStudiesForPatientByMRN;
-}
+const {
+ createGetImageSrcFromImageIdFn,
+ createRequestDisplaySetcreationFn,
+ getStudyForPatientUtility,
+} = studyPanelUtilities;
/**
* Wraps the PanelStudyBrowser and provides features afforded by managers/services
@@ -28,20 +24,14 @@ function WrappedPanelStudyBrowserTracking({
}) {
const dataSource = extensionManager.getActiveDataSource()[0];
- const getStudiesForPatientByMRN = _getStudyForPatientUtility(
- extensionManager
- );
- const _getStudiesForPatientByMRN = getStudiesForPatientByMRN.bind(
- null,
- dataSource
- );
- const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(
- extensionManager
- );
- const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
- null,
+ const _getStudiesForPatientByMRN = getStudyForPatientUtility(
+ extensionManager,
dataSource
);
+ const _getImageSrcFromImageId =
+ createGetImageSrcFromImageIdFn(extensionManager);
+ const _requestDisplaySetCreationForStudy =
+ createRequestDisplaySetcreationFn(dataSource);
return (
);
}
-/**
- * Grabs cornerstone library reference using a dependent command from
- * the @ohif/extension-cornerstone extension. Then creates a helper function
- * that can take an imageId and return an image src.
- *
- * @param {func} getCommand - CommandManager's getCommand method
- * @returns {func} getImageSrcFromImageId - A utility function powered by
- * cornerstone
- */
-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');
- }
-}
-
WrappedPanelStudyBrowserTracking.propTypes = {
commandsManager: PropTypes.object.isRequired,
extensionManager: PropTypes.object.isRequired,
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/index.js b/extensions/ohif-gradienthealth-extension/src/panels/index.js
index 7587823..5d7cb86 100644
--- a/extensions/ohif-gradienthealth-extension/src/panels/index.js
+++ b/extensions/ohif-gradienthealth-extension/src/panels/index.js
@@ -2,4 +2,11 @@ import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
import PanelMeasurementTableTracking from './PanelMeasurementTableTracking';
import PanelFormAndMeasurementTable from './PanelFormAndMeasurementTable';
import PanelForm from './PanelForm';
-export { PanelMeasurementTableTracking, PanelStudyBrowserTracking, PanelForm, PanelFormAndMeasurementTable };
+import PanelStudyBrowser from './PanelStudyBrowser';
+export {
+ PanelMeasurementTableTracking,
+ PanelStudyBrowserTracking,
+ PanelForm,
+ PanelFormAndMeasurementTable,
+ PanelStudyBrowser,
+};
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js b/extensions/ohif-gradienthealth-extension/src/panels/utils/getImageSrcFromImageId.ts
similarity index 56%
rename from extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js
rename to extensions/ohif-gradienthealth-extension/src/panels/utils/getImageSrcFromImageId.ts
index 40e9ae8..3a183ce 100644
--- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js
+++ b/extensions/ohif-gradienthealth-extension/src/panels/utils/getImageSrcFromImageId.ts
@@ -1,13 +1,9 @@
-/**
- * @param {*} cornerstone
- * @param {*} imageId
- */
-function getImageSrcFromImageId(cornerstone, imageId) {
+function getImageSrcFromImageId(cornerstone, imageId: string) {
return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas');
cornerstone.utilities
- .loadImageToCanvas({canvas, imageId})
- .then(imageId => {
+ .loadImageToCanvas({ canvas, imageId })
+ .then(() => {
resolve(canvas.toDataURL());
})
.catch(reject);
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/utils/index.ts b/extensions/ohif-gradienthealth-extension/src/panels/utils/index.ts
new file mode 100644
index 0000000..8358453
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/panels/utils/index.ts
@@ -0,0 +1,3 @@
+import * as studyPanelUtilities from './studyPanelUtilities';
+
+export { studyPanelUtilities };
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js b/extensions/ohif-gradienthealth-extension/src/panels/utils/requestDisplaySetCreationForStudy.ts
similarity index 70%
rename from extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js
rename to extensions/ohif-gradienthealth-extension/src/panels/utils/requestDisplaySetCreationForStudy.ts
index b6b79ec..093638e 100644
--- a/extensions/ohif-gradienthealth-extension/src/panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js
+++ b/extensions/ohif-gradienthealth-extension/src/panels/utils/requestDisplaySetCreationForStudy.ts
@@ -1,12 +1,12 @@
function requestDisplaySetCreationForStudy(
dataSource,
DisplaySetService,
- StudyInstanceUID,
- madeInClient
+ StudyInstanceUID: string,
+ madeInClient: boolean
) {
if (
DisplaySetService.activeDisplaySets.some(
- displaySet => displaySet.StudyInstanceUID === StudyInstanceUID
+ (displaySet) => displaySet.StudyInstanceUID === StudyInstanceUID
)
) {
return;
diff --git a/extensions/ohif-gradienthealth-extension/src/panels/utils/studyPanelUtilities.ts b/extensions/ohif-gradienthealth-extension/src/panels/utils/studyPanelUtilities.ts
new file mode 100644
index 0000000..34a63c9
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/panels/utils/studyPanelUtilities.ts
@@ -0,0 +1,28 @@
+import getImageSrcFromImageId from './getImageSrcFromImageId';
+import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
+
+export 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');
+ }
+}
+
+export function getStudyForPatientUtility(extensionManager, datasource) {
+ const utilityModule = extensionManager.getModuleEntry(
+ '@ohif/extension-default.utilityModule.common'
+ );
+
+ const { getStudiesForPatientByMRN } = utilityModule.exports;
+ return getStudiesForPatientByMRN.bind(null, datasource);
+}
+
+export function createRequestDisplaySetcreationFn(datasource) {
+ return requestDisplaySetCreationForStudy.bind(null, datasource);
+}
diff --git a/extensions/ohif-gradienthealth-extension/src/services/CacheAPIService/CacheAPIService.ts b/extensions/ohif-gradienthealth-extension/src/services/CacheAPIService/CacheAPIService.ts
index 4b5bfed..a1fc237 100644
--- a/extensions/ohif-gradienthealth-extension/src/services/CacheAPIService/CacheAPIService.ts
+++ b/extensions/ohif-gradienthealth-extension/src/services/CacheAPIService/CacheAPIService.ts
@@ -1,5 +1,5 @@
import { DicomMetadataStore, pubSubServiceInterface } from '@ohif/core';
-import { internal } from '@cornerstonejs/dicom-image-loader';
+import { internal, wadouri } from '@cornerstonejs/dicom-image-loader';
const { getOptions } = internal;
import _ from 'lodash';
import {
@@ -10,26 +10,32 @@ import {
imageLoadPoolManager,
} from '@cornerstonejs/core';
-const LOCAL_EVENTS = {};
+const LOCAL_EVENTS = {
+ IMAGE_CACHE_PREFETCHED: 'event::gradienthealth::image_cache_prefetched',
+};
export default class CacheAPIService {
listeners: { [key: string]: Function[] };
EVENTS: { [key: string]: string };
element: HTMLElement;
+ private servicesManager;
private commandsManager;
private extensionManager;
private dataSource;
private options;
public storageUsage;
public storageQuota;
+ private imageIdToFileUriMap;
constructor(servicesManager, commandsManager, extensionManager) {
this.listeners = {};
this.EVENTS = LOCAL_EVENTS;
this.commandsManager = commandsManager;
this.extensionManager = extensionManager;
+ this.servicesManager = servicesManager;
this.storageUsage = null;
this.storageQuota = null;
+ this.imageIdToFileUriMap = new Map();
Object.assign(this, pubSubServiceInterface);
}
@@ -119,13 +125,24 @@ export default class CacheAPIService {
});
}
- public async cacheStudy(StudyInstanceUID) {
- await this.dataSource.retrieve.series.metadata({ StudyInstanceUID });
+ public async cacheStudy(StudyInstanceUID, buckets = undefined) {
+ const segSOPClassUIDs = ['1.2.840.10008.5.1.4.1.1.66.4'];
+ await this.dataSource.retrieve.series.metadata({
+ StudyInstanceUID,
+ buckets,
+ });
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
- const imageIds = study.series.flatMap((serie) =>
- serie.instances.flatMap((instance) => instance.imageId)
- );
- this.cacheImageIds(imageIds);
+ const imageIds = study.series
+ .filter(
+ (serie) => !segSOPClassUIDs.includes(serie.instances[0].SOPClassUID)
+ )
+ .flatMap((serie) =>
+ serie.instances.flatMap((instance) => instance.imageId)
+ );
+ await Promise.all([
+ this.cacheImageIds(imageIds),
+ this.cacheSegFiles(StudyInstanceUID),
+ ]);
}
public async cacheSeries(StudyInstanceUID, SeriesInstanceUID) {
@@ -139,10 +156,17 @@ export default class CacheAPIService {
this.cacheImageIds(imageIds);
}
- public cacheImageIds(imageIds) {
+ public async cacheImageIds(imageIds) {
+ const promises: any[] = [];
+
function sendRequest(imageId, options) {
- return imageLoader.loadAndCacheImage(imageId, options).then(
- () => {},
+ const promise = imageLoader.loadAndCacheImage(imageId, options);
+ promises.push(promise);
+
+ return promise.then(
+ (imageLoadObject) => {
+ this._broadcastEvent(this.EVENTS.IMAGE_CACHE_PREFETCHED, { imageLoadObject });
+ },
(error) => {
console.error(error);
}
@@ -167,6 +191,57 @@ export default class CacheAPIService {
priority
);
});
+
+ await Promise.all(promises)
+ }
+
+ public async cacheSegFiles(studyInstanceUID) {
+ const segSOPClassUIDs = ['1.2.840.10008.5.1.4.1.1.66.4'];
+ const { displaySetService, userAuthenticationService } =
+ this.servicesManager.services;
+
+ const study = DicomMetadataStore.getStudy(studyInstanceUID);
+ const headers = userAuthenticationService.getAuthorizationHeader();
+ const promises = study.series.map((serie) => {
+ const { SOPClassUID, SeriesInstanceUID, url } = serie.instances[0];
+ if (segSOPClassUIDs.includes(SOPClassUID)) {
+ const { scheme, url: parsedUrl } = wadouri.parseImageId(url);
+ if (scheme === 'dicomzip') {
+ return wadouri.loadZipRequest(parsedUrl, url);
+ }
+
+ const displaySet =
+ displaySetService.getDisplaySetsForSeries(SeriesInstanceUID)[0];
+
+ if (this.imageIdToFileUriMap.get(url) === displaySet.instance.imageId) {
+ return;
+ }
+
+ return fetch(parsedUrl, { headers })
+ .then((response) => response.arrayBuffer())
+ .then((buffer) => wadouri.fileManager.add(new Blob([buffer])))
+ .then((fileUri) => {
+ this.imageIdToFileUriMap.set(url, fileUri);
+ displaySet.instance.imageId = fileUri;
+ displaySet.instance.getImageId = () => fileUri;
+ });
+ }
+ });
+
+ await Promise.all(promises);
+ }
+
+ public updateCachedFile(blob, displaySet) {
+ const { url, imageId } = displaySet.instances[0];
+ const fileUri = wadouri.fileManager.add(blob);
+ displaySet.instance.imageId = fileUri;
+ displaySet.instance.getImageId = () => fileUri;
+ this.imageIdToFileUriMap.set(url, fileUri);
+
+ if (imageId?.startsWith('dicomfile:')) {
+ const { url: index } = wadouri.parseImageId(imageId);
+ wadouri.fileManager.remove(index);
+ }
}
public async cacheMissingStudyImageIds(StudyInstanceUIDs) {
diff --git a/extensions/ohif-gradienthealth-extension/src/services/CropDisplayAreaService/CropDisplayAreaService.ts b/extensions/ohif-gradienthealth-extension/src/services/CropDisplayAreaService/CropDisplayAreaService.ts
index 1069c74..46003d6 100644
--- a/extensions/ohif-gradienthealth-extension/src/services/CropDisplayAreaService/CropDisplayAreaService.ts
+++ b/extensions/ohif-gradienthealth-extension/src/services/CropDisplayAreaService/CropDisplayAreaService.ts
@@ -1,188 +1,394 @@
import { pubSubServiceInterface } from '@ohif/core';
-import {
- EVENTS as CS_EVENTS,
- eventTarget as CornerstoneEventTarget,
- getEnabledElement
+import {
+ EVENTS as CS_EVENTS,
+ eventTarget as CornerstoneEventTarget,
+ getEnabledElement,
+ cache,
+ Enums as CSCORE_ENUMS
} from '@cornerstonejs/core';
+import { Enums as CSTOOLS_ENUMS } from '@cornerstonejs/tools';
import * as tf from '@tensorflow/tfjs';
-import { IStackViewport } from '@cornerstonejs/core/dist/esm/types';
+import {
+ IStackViewport,
+ IVolumeViewport,
+} from '@cornerstonejs/core/dist/esm/types';
+import { getSegDisplaysetsOfReferencedImagesIds } from '../utils';
const EVENTS = {
- CROP_DISPLAY_AREA_INIT: 'event::gradienthealth::CropDisplayAreaService:init',
+ CROP_DISPLAY_AREA_INIT: 'event::gradienthealth::CropDisplayAreaService:init',
};
export default class CropDisplayAreaService {
- private serviceManager;
- private listeners;
- public EVENTS;
-
- constructor(serviceManager) {
- this.serviceManager = serviceManager;
- this.listeners = {};
- this.EVENTS = EVENTS;
- window.tf = tf;
- Object.assign(this, pubSubServiceInterface);
- }
+ private serviceManager;
+ private listeners;
+ public EVENTS;
+
+ constructor(serviceManager) {
+ this.serviceManager = serviceManager;
+ this.listeners = {};
+ this.EVENTS = EVENTS;
+ window.tf = tf;
+ Object.assign(this, pubSubServiceInterface);
+ }
- init(){
- CornerstoneEventTarget.addEventListener(CS_EVENTS.STACK_VIEWPORT_NEW_STACK, (evt)=>{
+ init(){
+ CornerstoneEventTarget.addEventListener(CS_EVENTS.STACK_VIEWPORT_NEW_STACK, (evt)=>{
const { HangingProtocolService } = this.serviceManager.services
if(HangingProtocolService.protocol.id === 'breast') this.handleBreastDensityHP(evt)
})
- }
+ }
- private handleBreastDensityHP(evt){
- const { HangingProtocolService, cornerstoneViewportService } =
- this.serviceManager.services;
- const { element, viewportId } = evt.detail;
- const enabledElement = getEnabledElement(element);
- const viewport = enabledElement?.viewport;
- if (!viewport) return;
-
- const { voiRange, invert } = (viewport as IStackViewport).getProperties();
- let cutoff;
- if (voiRange?.lower && !invert) {
- cutoff = voiRange?.lower;
- }
- if (voiRange?.upper && invert) {
- cutoff = voiRange?.upper;
- }
- if (!cutoff) {
- return;
- }
+ private handleBreastDensityHP(evt){
+ const { HangingProtocolService, cornerstoneViewportService } =
+ this.serviceManager.services;
+ const { element, viewportId } = evt.detail;
+ const enabledElement = getEnabledElement(element);
+ const viewport = enabledElement?.viewport;
+ if (!viewport) return;
- const viewportInfo =
+ const viewportInfo =
cornerstoneViewportService.getViewportInfo(viewportId);
- const matchedDisplaySets = Array.from(
- HangingProtocolService.displaySetMatchDetails.values()
- );
- const matchedDisplaySetIndex = matchedDisplaySets.findIndex(
- (displayset) =>
- displayset.displaySetInstanceUID ===
- viewportInfo.viewportData.data.displaySetInstanceUID
- );
+ const matchedDisplaySets = Array.from(
+ HangingProtocolService.displaySetMatchDetails.values()
+ );
+ const matchedDisplaySetIndex = matchedDisplaySets.findIndex(
+ (displayset) =>
+ displayset.displaySetInstanceUID ===
+ viewportInfo.viewportData.data.displaySetInstanceUID
+ );
- const matchedDisplaySetKeys = Array.from(
- HangingProtocolService.displaySetMatchDetails.keys()
- );
- const matchedDisplaySet = matchedDisplaySetKeys[matchedDisplaySetIndex];
- if (!matchedDisplaySet) return;
-
- const imageData = viewport.getImageData();
- const scalarData = imageData?.scalarData;
- const dimensions = imageData?.dimensions;
- if (!scalarData || !dimensions) return;
-
- // probably will need to account for
- // imageData.direction
- // interesting that dim[1], dim[0] are reversed for vtk.js => tf.js
- // assume this direction does not change
- const { bboxWidth, bboxHeight, width, height } = tf.tidy(() => {
- const tensor = tf.tensor2d(new Float32Array(scalarData), [
- dimensions[1],
- dimensions[0],
- ]);
- const mask = tensor.greater(cutoff); // get boolean
- const widthBool = mask.any(0); // height?
- const heightBool = mask.any(1); // width?
-
- // get bbox
- const left = widthBool.argMax();
- const right = widthBool.reverse().argMax().mul(-1).add(widthBool.size);
- const top = heightBool.argMax();
- const bottom = heightBool
+ const matchedDisplaySetKeys = Array.from(
+ HangingProtocolService.displaySetMatchDetails.keys()
+ );
+ const matchedDisplaySet = matchedDisplaySetKeys[matchedDisplaySetIndex];
+ if (!matchedDisplaySet) return;
+
+ const imageData = viewport.getImageData();
+ const scalarData = imageData?.scalarData;
+ const dimensions = imageData?.dimensions;
+ if (!scalarData || !dimensions) return;
+
+ // probably will need to account for
+ // imageData.direction
+ // interesting that dim[1], dim[0] are reversed for vtk.js => tf.js
+ // assume this direction does not change
+ const { bboxWidth, bboxHeight, width, height } = tf.tidy(() => {
+ const tensor = tf.tensor2d(new Float32Array(scalarData), [
+ dimensions[1],
+ dimensions[0],
+ ]);
+ const cutoff = tensor.max().dataSync()[0] * 0.2; // 20% of max pixel value
+ const mask = tensor.greater(cutoff); // get boolean
+ const widthBool = mask.any(0); // height?
+ const heightBool = mask.any(1); // width?
+
+ // get bbox
+ const left = widthBool.argMax();
+ const right = widthBool.reverse().argMax().mul(-1).add(widthBool.size);
+ const top = heightBool.argMax();
+ const bottom = heightBool
.reverse()
.argMax()
.mul(-1)
.add(heightBool.size);
- // get percentage difference in width and height
- const bboxWidth = right.sub(left).dataSync()[0];
- const bboxHeight = bottom.sub(top).dataSync()[0];
- const width = widthBool.size;
- const height = heightBool.size;
-
- return {
- bboxWidth,
- bboxHeight,
- width,
- height,
- };
- });
-
- const bboxAspectRatio = bboxWidth / bboxHeight;
- const canvasAspectRatio = viewport.sWidth / viewport.sHeight;
- // console.log({bboxAspectRatio, canvasAspectRatio})
- // if(bboxAspectRatio > canvasAspectRatio){
- // bboxWidth = canvasAspectRatio*bboxHeight
- // bboxAspectRatio = bboxWidth/bboxHeight
- // console.log('changed', {bboxAspectRatio, canvasAspectRatio})
- // }
-
- const bboxWidthPercentage = bboxWidth / width; // add buffer
- const bboxHeightPercentage = bboxHeight / height;
-
- // TODO do not hard code, pick the max between bboxwidth and aspect ratio height
- const areaZoom = bboxWidthPercentage;
- //const panAmount = (1 - areaZoom) / 2;
-
- if (matchedDisplaySet === 'LMLO') {
- viewport.setDisplayArea(
- {
- imageArea: [areaZoom, areaZoom],
- imageCanvasPoint: {
- canvasPoint: [0, 0.5],
- imagePoint: [0, 0.5],
- },
- storeAsInitialCamera: true,
+ // get percentage difference in width and height
+ const bboxWidth = right.sub(left).dataSync()[0];
+ const bboxHeight = bottom.sub(top).dataSync()[0];
+ const width = widthBool.size;
+ const height = heightBool.size;
+
+ return {
+ bboxWidth,
+ bboxHeight,
+ width,
+ height,
+ };
+ });
+
+ const bboxAspectRatio = bboxWidth / bboxHeight;
+ const canvasAspectRatio = viewport.sWidth / viewport.sHeight;
+ // console.log({bboxAspectRatio, canvasAspectRatio})
+ // if(bboxAspectRatio > canvasAspectRatio){
+ // bboxWidth = canvasAspectRatio*bboxHeight
+ // bboxAspectRatio = bboxWidth/bboxHeight
+ // console.log('changed', {bboxAspectRatio, canvasAspectRatio})
+ // }
+
+ const bboxWidthPercentage = bboxWidth / width; // add buffer
+ const bboxHeightPercentage = bboxHeight / height;
+
+ // TODO do not hard code, pick the max between bboxwidth and aspect ratio height
+ const areaZoom = bboxWidthPercentage;
+ //const panAmount = (1 - areaZoom) / 2;
+
+ if (matchedDisplaySet === 'LMLO') {
+ viewport.setDisplayArea(
+ {
+ imageArea: [areaZoom, areaZoom],
+ imageCanvasPoint: {
+ canvasPoint: [0, 0.5],
+ imagePoint: [0, 0.5],
},
- true
- );
- }
- if (matchedDisplaySet === 'RMLO') {
- viewport.setDisplayArea(
- {
- imageArea: [areaZoom, areaZoom],
- imageCanvasPoint: {
- canvasPoint: [1, 0.5],
- imagePoint: [1, 0.5],
- },
- storeAsInitialCamera: true,
+ storeAsInitialCamera: true,
+ },
+ true
+ );
+ }
+ if (matchedDisplaySet === 'RMLO') {
+ viewport.setDisplayArea(
+ {
+ imageArea: [areaZoom, areaZoom],
+ imageCanvasPoint: {
+ canvasPoint: [1, 0.5],
+ imagePoint: [1, 0.5],
},
+ storeAsInitialCamera: true,
+ },
- true
- );
- }
- if (matchedDisplaySet === 'LCC') {
- viewport.setDisplayArea(
- {
- imageArea: [areaZoom, areaZoom],
- imageCanvasPoint: {
- canvasPoint: [0, 0.5],
- imagePoint: [0, 0.5],
- },
- storeAsInitialCamera: true,
+ true
+ );
+ }
+ if (matchedDisplaySet === 'LCC') {
+ viewport.setDisplayArea(
+ {
+ imageArea: [areaZoom, areaZoom],
+ imageCanvasPoint: {
+ canvasPoint: [0, 0.5],
+ imagePoint: [0, 0.5],
},
- true
- );
- }
- if (matchedDisplaySet === 'RCC') {
- viewport.setDisplayArea(
- {
- imageArea: [areaZoom, areaZoom],
- imageCanvasPoint: {
- canvasPoint: [1, 0.5],
- imagePoint: [1, 0.5],
- },
- storeAsInitialCamera: true,
+ storeAsInitialCamera: true,
+ },
+ true
+ );
+ }
+ if (matchedDisplaySet === 'RCC') {
+ viewport.setDisplayArea(
+ {
+ imageArea: [areaZoom, areaZoom],
+ imageCanvasPoint: {
+ canvasPoint: [1, 0.5],
+ imagePoint: [1, 0.5],
},
- true
+ storeAsInitialCamera: true,
+ },
+ true
+ );
+ }
+ }
+
+ destroy() {
+ }
+
+ public async focusToSegment(segmentationId, segmentIndex) {
+ const {
+ segmentationService,
+ viewportGridService,
+ cornerstoneViewportService,
+ displaySetService,
+ } = this.serviceManager.services;
+
+ const segmentation = segmentationService.getSegmentation(segmentationId);
+ const segDisplayset = displaySetService.getDisplaySetByUID(segmentation.displaySetInstanceUID);
+ if (segDisplayset.Modality !== 'SEG') {
+ return;
+ }
+
+ const imageIdReferenceMap =
+ segmentation?.representationData[segmentation.type].imageIdReferenceMap;
+
+ segmentIndex = segmentIndex || segmentation.activeSegmentIndex;
+
+ let dimensions, pixelData;
+
+ if (imageIdReferenceMap) {
+ const image = cache.getImage(imageIdReferenceMap.values().next().value);
+ const { rows, columns } = image;
+ dimensions = [columns, rows, imageIdReferenceMap.size];
+ pixelData = image.getPixelData();
+ } else {
+ const volume = cache.getVolume(segmentationId);
+ ({ dimensions } = volume);
+ pixelData = volume.scalarData;
+ }
+
+ const mask = tf.tidy(() => {
+ let tensor;
+ if (imageIdReferenceMap) {
+ tensor = tf.tensor2d(new Float32Array(pixelData), [
+ dimensions[1],
+ dimensions[0],
+ ]);
+ } else {
+ tensor = tf.tensor3d(new Float32Array(pixelData), [
+ dimensions[2],
+ dimensions[0],
+ dimensions[1],
+ ]);
+ }
+
+ return tensor.equal(segmentIndex); // get boolean
+ });
+
+ const maskCoordinates = await tf.whereAsync(mask);
+
+ const { xMax, yMax, xMin, yMin } = tf.tidy(() => {
+ const transpose = tf.einsum('ij->ji', maskCoordinates);
+ tf.dispose(mask);
+ tf.dispose(maskCoordinates);
+
+ let xMin = 0,
+ xMax = dimensions[0],
+ yMin = 0,
+ yMax = dimensions[1];
+
+ if (transpose.size !== 0) {
+ if (imageIdReferenceMap) {
+ xMin = transpose.gather(1).min().dataSync()[0];
+ xMax = transpose.gather(1).max().dataSync()[0];
+ yMin = transpose.gather(0).min().dataSync()[0];
+ yMax = transpose.gather(0).max().dataSync()[0];
+ } else {
+ xMin = transpose.gather(2).min().dataSync()[0];
+ xMax = transpose.gather(2).max().dataSync()[0];
+ yMin = transpose.gather(1).min().dataSync()[0];
+ yMax = transpose.gather(1).max().dataSync()[0];
+ }
+ }
+
+ return { xMax, yMax, xMin, yMin };
+ });
+
+ const referencedDisplaySetInstanceUID = segDisplayset.referencedDisplaySetInstanceUID;
+ const { viewports, activeViewportId } = viewportGridService.getState();
+ const viewportsWithSegmentation: IStackViewport | IVolumeViewport = [];
+ viewports.forEach((viewport) => {
+ if (viewport.displaySetInstanceUIDs.includes(referencedDisplaySetInstanceUID)) {
+ viewportsWithSegmentation.push(
+ cornerstoneViewportService.getCornerstoneViewport(viewport.viewportId)
);
}
- }
+ });
+
+ let bboxWidth = xMax + 1 - xMin;
+ let bboxHeight = yMax + 1 - yMin;
+ let width = dimensions[0];
+ let height = dimensions[1];
+ const imageAspectRatio = width / height;
+
+ const imagePoint = [
+ (xMax + xMin) / (2 * width),
+ (yMax + yMin) / (2 * height),
+ ] as [number, number];
+ const zoomFactors = {
+ x: bboxWidth / width,
+ y: bboxHeight / height,
+ };
+
+ viewportsWithSegmentation.forEach((viewport) => {
+ const canvasAspectRatio = viewport.sWidth / viewport.sHeight;
+ const zoomFactorsCopy = { ...zoomFactors };
+ correctZoomFactors(zoomFactorsCopy, imageAspectRatio, canvasAspectRatio);
- destroy() {
+ setDisplayArea(viewport, zoomFactorsCopy, imagePoint);
+ });
+
+ if (!viewportsWithSegmentation.length) {
+ const activeViewport =
+ cornerstoneViewportService.getCornerstoneViewport(activeViewportId);
+
+ handleFocusingForNewStack(
+ activeViewport,
+ displaySetService,
+ zoomFactors,
+ imagePoint,
+ imageAspectRatio
+ );
}
}
-
\ No newline at end of file
+}
+
+const setDisplayArea = (
+ viewport: IStackViewport | IVolumeViewport,
+ zoomFactors: { x: number; y: number },
+ imagePoint: [number, number]
+) => {
+ viewport.setDisplayArea({
+ imageArea: <[number, number]>[zoomFactors.x, zoomFactors.y],
+ imageCanvasPoint: { imagePoint, canvasPoint: <[number, number]>[0.5, 0.5] },
+ });
+ viewport.render();
+};
+
+const handleFocusingForNewStack = (
+ viewport: IStackViewport | IVolumeViewport,
+ displaySetService: any,
+ zoomFactors: { x: number; y: number },
+ imagePoint: [number, number],
+ imageAspectRatio: number
+) => {
+ const canvasAspectRatio = viewport.sWidth / viewport.sHeight;
+
+ const eventElement =
+ viewport.type === CSCORE_ENUMS.ViewportType.STACK
+ ? CornerstoneEventTarget
+ : viewport.element;
+ const eventName =
+ viewport.type === CSCORE_ENUMS.ViewportType.STACK
+ ? CS_EVENTS.STACK_VIEWPORT_NEW_STACK
+ : CS_EVENTS.VOLUME_VIEWPORT_NEW_VOLUME;
+
+ const newImageListener = (evt) => {
+ const segDisplaySetsOfLoadedSeries = getSegDisplaysetsOfReferencedImagesIds(
+ evt.detail.imageIds,
+ displaySetService
+ );
+
+ let segmentationsRenderedCount = 0;
+ const segmentationRenderedListener = () => {
+ if (
+ ++segmentationsRenderedCount === segDisplaySetsOfLoadedSeries.length
+ ) {
+ correctZoomFactors(zoomFactors, imageAspectRatio, canvasAspectRatio);
+ setDisplayArea(viewport, zoomFactors, imagePoint);
+
+ CornerstoneEventTarget.removeEventListener(
+ CSTOOLS_ENUMS.Events.SEGMENTATION_RENDERED,
+ segmentationRenderedListener
+ );
+ }
+ };
+
+ CornerstoneEventTarget.addEventListener(
+ CSTOOLS_ENUMS.Events.SEGMENTATION_RENDERED,
+ segmentationRenderedListener
+ );
+
+ eventElement.removeEventListener(eventName, newImageListener);
+ };
+
+ eventElement.addEventListener(eventName, newImageListener);
+};
+
+const correctZoomFactors = (
+ zoomFactors: { x: number; y: number },
+ imageAspectRatio: number,
+ canvasAspectRatio: number
+) => {
+ if (imageAspectRatio < canvasAspectRatio) {
+ zoomFactors.x /= canvasAspectRatio / imageAspectRatio;
+ }
+ if (imageAspectRatio > canvasAspectRatio) {
+ zoomFactors.y /= imageAspectRatio / canvasAspectRatio;
+ }
+
+ if (zoomFactors.x > 0.8 || zoomFactors.y > 0.8) {
+ return;
+ }
+
+ const zoomOutPercentatage = 80;
+
+ zoomFactors.x /= zoomOutPercentatage / 100;
+ zoomFactors.y /= zoomOutPercentatage / 100;
+};
\ No newline at end of file
diff --git a/extensions/ohif-gradienthealth-extension/src/services/GoogleSheetsService/GoogleSheetsService.js b/extensions/ohif-gradienthealth-extension/src/services/GoogleSheetsService/GoogleSheetsService.js
index a468d0f..231659a 100644
--- a/extensions/ohif-gradienthealth-extension/src/services/GoogleSheetsService/GoogleSheetsService.js
+++ b/extensions/ohif-gradienthealth-extension/src/services/GoogleSheetsService/GoogleSheetsService.js
@@ -1,3 +1,5 @@
+import { eventTarget, Enums, cache, metaData } from '@cornerstonejs/core';
+import { utilities as csToolsUtils } from '@cornerstonejs/tools';
import { DicomMetadataStore, pubSubServiceInterface } from '@ohif/core';
import { alphabet } from './utils';
@@ -53,12 +55,26 @@ export default class GoogleSheetsService {
const min = index - bufferBack < 2 ? 2 : index - bufferBack;
const max = index + bufferFront;
const urlIndex = this.formHeader.findIndex((name) => name == 'URL');
- this.rows.slice(min, max).forEach((row) => {
- const url = row[urlIndex];
- const params = new URLSearchParams('?' + url.split('?')[1]);
- const StudyInstanceUID = params.get('StudyInstanceUIDs');
- CacheAPIService.cacheStudy(StudyInstanceUID);
- });
+ const studyIdIndex = this.formHeader.findIndex((name) => name == 'ID');
+
+ const rowsToCache = this.rows.slice(min - 1, max);
+ const indexOfCurrentId = rowsToCache.findIndex(
+ (row) => row[studyIdIndex] === id
+ );
+ const element = rowsToCache.splice(indexOfCurrentId, 1);
+ rowsToCache.unshift(element[0]); // making the current studyid as first element
+
+ rowsToCache.reduce((promise, row) => {
+ return promise.then(() => {
+ const url = row[urlIndex];
+ const params = new URLSearchParams('?' + url.split('?')[1]);
+ const StudyInstanceUID = getStudyInstanceUIDFromParams(params);
+ return CacheAPIService.cacheStudy(
+ StudyInstanceUID,
+ params.getAll('bucket')
+ );
+ });
+ }, Promise.resolve());
}
setFormByStudyInstanceUID(id) {
@@ -80,6 +96,16 @@ export default class GoogleSheetsService {
this.user = UserAuthenticationService.getUser();
const params = new URLSearchParams(window.location.search);
+ if (window.location.pathname.includes('/segmentation')) {
+ // Since sheet panel only used by segmentation and breast density mode,
+ // and breast density mode does not handles segmentation we are only loading
+ // segmentations in segmentation mode.
+ eventTarget.addEventListener(
+ Enums.Events.STACK_VIEWPORT_NEW_STACK,
+ () => loadSegFiles(this.serviceManager)
+ );
+ }
+
if (!params.get('sheetId'))
return this._broadcastEvent(EVENTS.GOOGLE_SHEETS_ERROR);
if (!params.get('sheetName'))
@@ -101,14 +127,14 @@ export default class GoogleSheetsService {
this.studyUIDToIndex = this.rows.slice(1).reduce((prev, curr, idx) => {
const url = curr[urlIndex];
const params = new URLSearchParams('?' + url.split('?')[1]);
- const StudyInstanceUID = params.get('StudyInstanceUIDs');
+ const StudyInstanceUID = getStudyInstanceUIDFromParams(params);
// Google Sheets is 1-indexed and we ignore first row as header row thus + 2
prev[StudyInstanceUID] = idx + 2;
return prev;
}, {});
- this.index = this.studyUIDToIndex[params.get('StudyInstanceUIDs')];
+ this.index = this.studyUIDToIndex[getStudyInstanceUIDFromParams(params)];
// Map formTemplate and formValue
const values = this.settings.values[0].map((_, colIndex) =>
@@ -143,7 +169,7 @@ export default class GoogleSheetsService {
})
.sort((a, b) => a.order - b.order);
- this.setFormByStudyInstanceUID(params.get('StudyInstanceUIDs'));
+ this.setFormByStudyInstanceUID(getStudyInstanceUIDFromParams(params));
} catch (e) {
console.error(e);
this._broadcastEvent(EVENTS.GOOGLE_SHEETS_ERROR);
@@ -204,7 +230,7 @@ export default class GoogleSheetsService {
});
}
- async writeFormToRow(formValue) {
+ async updateRow(formValue) {
const values = this.formHeader.map((colName) => {
const index = this.formTemplate.findIndex((ele) => {
return colName == ele.name;
@@ -225,6 +251,14 @@ export default class GoogleSheetsService {
return null;
});
+ // google sheets is 1-indexed, so take rows[index-1]
+ const updatedFormValue = this.rows[this.index - 1].map((element, index) =>
+ values[index] !== null ? values[index] : element
+ );
+
+ this.rows[this.index - 1] = updatedFormValue;
+ this.formValue = formValue
+
await this.writeRange(
this.sheetId,
this.sheetName,
@@ -244,8 +278,12 @@ export default class GoogleSheetsService {
async getRow(delta) {
try {
- const { DisplaySetService, HangingProtocolService, CacheAPIService } =
- this.serviceManager.services;
+ const {
+ DisplaySetService,
+ HangingProtocolService,
+ CacheAPIService,
+ SegmentationService,
+ } = this.serviceManager.services;
const rowValues = this.rows[this.index + delta - 1];
if (!rowValues) {
window.location.href = `https://docs.google.com/spreadsheets/d/${this.sheetId}`;
@@ -253,13 +291,17 @@ export default class GoogleSheetsService {
const index = this.formHeader.findIndex((name) => name == 'URL');
const url = rowValues[index];
const params = new URLSearchParams('?' + url.split('?')[1]);
- const StudyInstanceUID = params.get('StudyInstanceUIDs');
+ const StudyInstanceUID = getStudyInstanceUIDFromParams(params);
+ const buckets = params.getAll('bucket');
if (!StudyInstanceUID) {
window.location.href = `https://docs.google.com/spreadsheets/d/${this.sheetId}`;
}
const dataSource = this.extensionManager.getActiveDataSource()[0];
- await dataSource.retrieve.series.metadata({ StudyInstanceUID });
+ await dataSource.retrieve.series.metadata({ StudyInstanceUID, buckets });
const studies = [DicomMetadataStore.getStudy(StudyInstanceUID)];
+ const activeProtocolId =
+ HangingProtocolService.getActiveProtocol().protocol.id;
+ HangingProtocolService.reset();
HangingProtocolService.run(
{
studies,
@@ -270,11 +312,24 @@ export default class GoogleSheetsService {
}
),
},
- 'breast'
+ activeProtocolId
+ );
+ const segmentations = SegmentationService.getSegmentations();
+ segmentations.forEach((segmentation) =>
+ SegmentationService.remove(segmentation.id)
);
const nextParams = new URLSearchParams(window.location.search);
- nextParams.set('StudyInstanceUIDs', StudyInstanceUID);
+ if (nextParams.get('StudyInstanceUIDs'))
+ nextParams.set('StudyInstanceUIDs', StudyInstanceUID);
+ else {
+ nextParams.set('StudyInstanceUID', StudyInstanceUID);
+ }
+ nextParams.delete('bucket');
+ buckets.forEach((bucket) => {
+ nextParams.append('bucket', bucket);
+ });
+
const nextURL =
window.location.href.split('?')[0] + '?' + nextParams.toString();
window.history.replaceState({}, null, nextURL);
@@ -294,3 +349,156 @@ export default class GoogleSheetsService {
this._broadcastEvent(EVENTS.GOOGLE_SHEETS_ERROR);
}
}
+
+function loadSegFiles(serviceManager) {
+ const params = new URLSearchParams(window.location.search);
+ const studyInstanceUID = getStudyInstanceUIDFromParams(params);
+
+ const segSOPClassUIDs = ['1.2.840.10008.5.1.4.1.1.66.4'];
+ const {
+ segmentationService,
+ displaySetService,
+ UserAuthenticationService,
+ CacheAPIService,
+ viewportGridService,
+ cornerstoneViewportService,
+ customizationService,
+ } = serviceManager.services;
+ const utilityModule = extensionManager.getModuleEntry(
+ '@ohif/extension-cornerstone.utilityModule.common'
+ );
+ const { getImageFlips } = utilityModule.exports;
+ const headers = UserAuthenticationService.getAuthorizationHeader();
+
+ const activeStudySegDisplaySets = displaySetService.getDisplaySetsBy(
+ (ds) =>
+ ds.StudyInstanceUID === studyInstanceUID &&
+ segSOPClassUIDs.includes(ds.SOPClassUID)
+ );
+ const nonSegImageIds = displaySetService
+ .getDisplaySetsBy(
+ (ds) =>
+ ds.StudyInstanceUID === studyInstanceUID &&
+ !segSOPClassUIDs.includes(ds.SOPClassUID)
+ )
+ .flatMap((ds) => ds.images.flatMap((image) => image.imageId));
+
+ const stackImageAddedCallback = (evt) => {
+ const viewport = cornerstoneViewportService.getCornerstoneViewport(
+ evt.detail.viewportId
+ );
+
+ const { criteria: isOrientationCorrectionNeeded } =
+ customizationService.get('orientationCorrectionCriterion');
+ const instance = metaData.get('instance', viewport.getCurrentImageId());
+
+ if (isOrientationCorrectionNeeded?.(instance)) {
+ const { hFlip, vFlip } = getImageFlips(instance);
+ (hFlip || vFlip) &&
+ viewport.setCamera({ flipHorizontal: hFlip, flipVertical: vFlip });
+ }
+
+ eventTarget.removeEventListener(
+ Enums.Events.STACK_VIEWPORT_IMAGES_ADDED,
+ stackImageAddedCallback
+ );
+ };
+
+ const isAllSegmentationsLoaded = isAllSegmentationsOfSeriesLoaded(
+ activeStudySegDisplaySets,
+ serviceManager
+ );
+
+ if (isAllSegmentationsLoaded) {
+ const renderedToolGroupIds = [];
+
+ activeStudySegDisplaySets.forEach((ds) => {
+ const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
+ ds.displaySetInstanceUID
+ );
+ toolGroupIds.forEach((toolGroupId) => {
+ if (renderedToolGroupIds.includes(toolGroupId)) {
+ return;
+ }
+
+ csToolsUtils.segmentation.triggerSegmentationRender(toolGroupId);
+ renderedToolGroupIds.push(toolGroupId);
+ });
+ });
+
+ return;
+ }
+
+ const isAllSeriesOfStudyCached = () => {
+ return nonSegImageIds.every((imageId) => cache.getImageLoadObject(imageId));
+ };
+
+ let unsubscribe;
+
+ const loadSegmentations = async () => {
+ if (isAllSeriesOfStudyCached()) {
+ const loadPromises = activeStudySegDisplaySets.map(async (displaySet) => {
+ displaySet.getReferenceDisplaySet();
+ return displaySet.load({ headers });
+ });
+
+ await Promise.all(loadPromises);
+
+ eventTarget.addEventListener(
+ Enums.Events.STACK_VIEWPORT_IMAGES_ADDED,
+ stackImageAddedCallback
+ );
+
+ const addRepresentationPromises = activeStudySegDisplaySets.map(
+ async (displaySet) =>
+ await segmentationService.addSegmentationRepresentationToToolGroup(
+ 'default',
+ displaySet.displaySetInstanceUID,
+ true
+ )
+ );
+
+ Promise.all(addRepresentationPromises).then(() => {
+ const { viewports, activeViewportId } = viewportGridService.getState();
+ const activeViewport = viewports.get(activeViewportId);
+ const segmentationsOfLoadedImage = displaySetService.getDisplaySetsBy(
+ (ds) =>
+ ds.referencedDisplaySetInstanceUID ===
+ activeViewport.displaySetInstanceUIDs[0]
+ );
+
+ // we are setting first segmentation of the image in the active viewport as active.
+ segmentationService.setActiveSegmentationForToolGroup(
+ segmentationsOfLoadedImage[0].displaySetInstanceUID
+ );
+ });
+
+ unsubscribe?.();
+ }
+ };
+
+ if (isAllSeriesOfStudyCached()) {
+ loadSegmentations();
+ } else {
+ ({ unsubscribe } = CacheAPIService.subscribe(
+ CacheAPIService.EVENTS.IMAGE_CACHE_PREFETCHED,
+ loadSegmentations
+ ));
+ }
+}
+
+function isAllSegmentationsOfSeriesLoaded(
+ activeStudySegDisplaySets,
+ servicesManager
+) {
+ const { segmentationService } = servicesManager.services;
+
+ return activeStudySegDisplaySets.every((ds) =>
+ segmentationService.getSegmentation(ds.displaySetInstanceUID)
+ );
+}
+
+function getStudyInstanceUIDFromParams(params) {
+ // Breast OHIF dicomweb datasource uses StudyInstanceUIDs, but bq datasource uses StudyInstanceUID
+ return params.get('StudyInstanceUIDs') || params.get('StudyInstanceUID');
+}
diff --git a/extensions/ohif-gradienthealth-extension/src/services/utils.ts b/extensions/ohif-gradienthealth-extension/src/services/utils.ts
new file mode 100644
index 0000000..7ea1241
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/services/utils.ts
@@ -0,0 +1,13 @@
+export const getSegDisplaysetsOfReferencedImagesIds = (
+ imageIds: string[] = [],
+ displaySetService: any
+) => {
+ const loadedDisplaySet = displaySetService.getDisplaySetsBy((ds) =>
+ ds.images?.find((image) => imageIds.includes(image.imageId))
+ )?.[0];
+
+ const referencedSeriesInstanceUID = loadedDisplaySet.SeriesInstanceUID;
+ return displaySetService.getDisplaySetsBy(
+ (ds) => ds.referencedSeriesInstanceUID === referencedSeriesInstanceUID
+ );
+};
diff --git a/extensions/ohif-gradienthealth-extension/src/utils/addSegmentationLabelModifier.ts b/extensions/ohif-gradienthealth-extension/src/utils/addSegmentationLabelModifier.ts
new file mode 100644
index 0000000..7e8d794
--- /dev/null
+++ b/extensions/ohif-gradienthealth-extension/src/utils/addSegmentationLabelModifier.ts
@@ -0,0 +1,40 @@
+const addSegmentationLabelModifier = (servicesManager) => {
+ const { segmentationService, displaySetService } = servicesManager.services;
+
+ segmentationService.subscribe(
+ segmentationService.EVENTS.SEGMENTATION_ADDED,
+ ({ segmentation }) => {
+ let displaySet = displaySetService.getDisplaySetByUID(
+ segmentation.displaySetInstanceUID
+ );
+
+ if (displaySet.Modality === 'SEG') {
+ return;
+ }
+
+ const descriptionComponents = [
+ displaySet.instances[0].ImageLaterality,
+ displaySet.instances[0].ViewPosition,
+ displaySet.SeriesDescription?.replace(/[/]/, ''),
+ ];
+ const newDescription = descriptionComponents
+ .filter((variable) => variable !== undefined)
+ .join('-');
+
+ const segmentationsCount =
+ segmentationService.getSegmentations(false).length;
+ const increment = segmentationsCount > 0 ? ' ' + segmentationsCount : '';
+
+ const label = `${newDescription} - Vessel ${increment}`;
+ segmentation.label = label;
+
+ segmentationService.addOrUpdateSegmentation(
+ segmentation,
+ false, // suppress event
+ true // notYetUpdatedAtSource
+ );
+ }
+ );
+};
+
+export default addSegmentationLabelModifier;