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;