Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { utils } from '@ohif/core';
import { metaData, triggerEvent, eventTarget } from '@cornerstonejs/core';
import { CONSTANTS, segmentation as cstSegmentation } from '@cornerstonejs/tools';
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
import { internal } from '@cornerstonejs/dicom-image-loader';

import { SOPClassHandlerId } from './id';
import { dicomlabToRGB } from './utils/dicomlabToRGB';
Expand Down Expand Up @@ -97,8 +98,10 @@ function _getDisplaySetsFromSeries(
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
}

const codHeaders = internal.getCodHeaders();

displaySet.load = async ({ headers }) =>
await _load(displaySet, servicesManager, extensionManager, headers);
await _load(displaySet, servicesManager, extensionManager, { ...headers, ...codHeaders });

return [displaySet];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ import { Study } from './types';
import {
clearPreviousOPFSVersionData,
deleteFoldersFromOPFS,
formatSize,
getOPFSData,
hybridGlobalFilter,
purgeOldFilesFromOPFS,
} from './utils';
import { OPFS_PURGE_METADATA } from './constants';

const columnHelper = createColumnHelper<Study>();

Expand Down Expand Up @@ -66,23 +69,21 @@ const columns: ColumnDef<Study>[] = [
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
StudyInstanceUID
<ArrowUpDown className="h-4 w-4" />
StudyInstanceUID <ArrowUpDown className="h-4 w-4" />
</Button>
);
},
cell: ({ row }) => <div>{row.getValue('study-uid')}</div>,
},
{
accessorKey: 'study-description',
columnHelper.accessor('study-description', {
accessorFn: row => row['study-description'] || '',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
StudyDescription
<ArrowUpDown className="h-4 w-4" />
StudyDescription <ArrowUpDown className="h-4 w-4" />
</Button>
);
},
Expand All @@ -97,7 +98,7 @@ const columns: ColumnDef<Study>[] = [
</Tooltip>
);
},
},
}),
columnHelper.accessor('study-modalities', {
accessorFn: row => row['study-modalities'].sort().join(', '),
header: ({ column }) => {
Expand All @@ -106,8 +107,7 @@ const columns: ColumnDef<Study>[] = [
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Modalities
<ArrowUpDown className="h-4 w-4" />
Modalities <ArrowUpDown className="h-4 w-4" />
</Button>
);
},
Expand All @@ -126,18 +126,7 @@ const columns: ColumnDef<Study>[] = [
columnHelper.accessor('study-size', {
accessorFn: row => {
const size: number = row['study-size'];
const oneGB = 1024 * 1024 * 1024,
oneMB = 1024 * 1024,
oneKB = 1024;

if (size >= oneGB) {
return `${(size / oneGB).toFixed(2)} GB`;
} else if (size >= oneMB) {
return `${(size / oneMB).toFixed(2)} MB`;
} else if (size >= oneKB) {
return `${(size / oneKB).toFixed(2)} KB`;
}
return `${size} bytes`;
return formatSize(size);
},
sortingFn: (rowA, rowB, columnId) => {
const sizeA = rowA.original[columnId];
Expand All @@ -157,27 +146,30 @@ const columns: ColumnDef<Study>[] = [
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Size
<ArrowUpDown className="h-4 w-4" />
Size <ArrowUpDown className="h-4 w-4" />
</Button>
);
},
cell: ({ row }) => <div>{row.getValue('study-size')}</div>,
}),
columnHelper.accessor('study-last-modified', {
accessorFn: row => new Date(row['study-last-modified']).toGMTString(),
accessorFn: row => new Date(row['study-last-modified']),
sortingFn: 'datetime',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Last Modified
<ArrowUpDown className="h-4 w-4" />
Last Modified <ArrowUpDown className="h-4 w-4" />
</Button>
);
},
cell: ({ row }) => <div>{row.getValue('study-last-modified')}</div>,
cell: ({ row }) => {
const value: Date = row.getValue('study-last-modified');
const formattedDate = value.toDateString() + ', ' + value.toLocaleTimeString();
return <div>{formattedDate}</div>;
},
}),
{
id: 'actions',
Expand Down Expand Up @@ -247,6 +239,7 @@ export default function OPFSManagementTool() {

const refreshOPFSData = async () => {
const fetchedData = await getOPFSData();
table.toggleAllPageRowsSelected(false);
setData(fetchedData);
};

Expand All @@ -265,6 +258,19 @@ export default function OPFSManagementTool() {
}
};

const purgeOldFiles = async (time: number) => {
await purgeOldFilesFromOPFS(time);
refreshOPFSData();
};

const calculateTotalSize = (list: Study[]) => {
const totalSize = list.reduce((total, study) => {
return total + study['study-size'];
}, 0);

return formatSize(totalSize);
};

return (
<div className="w-full p-6">
<div className="flex items-center py-4">
Expand Down Expand Up @@ -300,6 +306,28 @@ export default function OPFSManagementTool() {
>
Debug Copy
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="ml-2"
>
Purge <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Files older than</DropdownMenuLabel>
{OPFS_PURGE_METADATA.map(option => (
<DropdownMenuItem
key={option.label}
className="capitalize"
onClick={() => purgeOldFiles(option.time)}
>
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
Expand Down Expand Up @@ -378,6 +406,12 @@ export default function OPFSManagementTool() {
<div className="text-muted-foreground flex-1 text-sm">
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
{table.getFilteredSelectedRowModel().rows.length
? ` ${calculateTotalSize(data.filter((study, index) => rowSelection[index]))}.`
: ''}
</div>
<div className="text-muted-foreground flex-1 text-sm">
Total size: {calculateTotalSize(data)}
</div>
<div className="space-x-2">
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
export const OPFS_VERSION_STORAGE_KEY = 'gh-opfs-path-version';
export const CURRENT_OPFS_VERSION = 1;

export const OPFS_PURGE_METADATA = [
{
label: 'All',
time: null,
},
{
label: '1 Hour',
time: 1000 * 60 * 60,
},
{
label: '4 Hours',
time: 1000 * 60 * 60 * 4,
},
{
label: '12 Hours',
time: 1000 * 60 * 60 * 12,
},
{
label: '1 Day',
time: 1000 * 60 * 60 * 24,
},
{
label: '1 Week',
time: 1000 * 60 * 60 * 24 * 7,
},
];
78 changes: 75 additions & 3 deletions extensions/cornerstone/src/components/OPFSManagementTool/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ function structureData(fileDetails: FileDetails[]): Study[] {
}

const firstInstance = Object.values(metadataFile.data.cod.instances)[0];
const studyDescription = firstInstance.metadata['00081030']?.Value[0];
const seriesDescription = firstInstance.metadata['0008103E']?.Value[0];
const seriesModality = firstInstance.metadata['00080060']?.Value[0];
const studyDescription = firstInstance.metadata['00081030']?.Value?.[0];
const seriesDescription = firstInstance.metadata['0008103E']?.Value?.[0];
const seriesModality = firstInstance.metadata['00080060']?.Value?.[0];

let totalSeriesSize = 0;
let seriesMostRecentModified = 0;
Expand Down Expand Up @@ -274,6 +274,63 @@ export async function deleteFoldersFromOPFS(folderPaths: string[]) {
}
}

export async function purgeOldFilesFromOPFS(maxAgeMs?: number): Promise<void> {
// If the maxAgeMs value is null, clear the OPFS.
// If the user selected to purge 'All' OPFS data, the maxAgeMs will be null.
if (!maxAgeMs) {
try {
const rootHandle = await getOPFSRootHandle();
// @ts-ignore
await rootHandle.remove({ recursive: true });
} catch (error) {
console.warn(`Error purging files: ${error.message}`);
}
return;
}

const cutoffTime = Date.now() - maxAgeMs;

async function traverseAndClean(dirHandle: FileSystemDirectoryHandle): Promise<void> {
const entries: (FileSystemFileHandle | FileSystemDirectoryHandle)[] = [];
// @ts-ignore
for await (const subDirHandle of dirHandle.values()) {
entries.push(subDirHandle);
}

await Promise.all(
entries.map(async subDirHandle => {
if (!maxAgeMs) {
await dirHandle.removeEntry(subDirHandle.name, { recursive: true });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe remove this if statement?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if statement is necessary. I have added a comment to describe the use.


if (subDirHandle.kind === 'file') {
const fileHandle = subDirHandle as FileSystemFileHandle;
const file = await fileHandle.getFile();

if (file.lastModified < cutoffTime) {
await dirHandle.removeEntry(file.name);
}
} else if (subDirHandle.kind === 'directory') {
await traverseAndClean(subDirHandle);

// @ts-ignore
if ((await subDirHandle.values().next()).done) {
// Subdirectory is empty: DELETE it from the parent
await dirHandle.removeEntry(subDirHandle.name);
}
}
})
);
}

try {
const rootHandle = await getOPFSRootHandle();
await traverseAndClean(rootHandle);
} catch (error) {
console.warn(`Error clearing partial files: ${error.message}`);
}
}

export function hybridGlobalFilter(
row: Row<Study>,
columnId: string,
Expand All @@ -297,3 +354,18 @@ export function hybridGlobalFilter(
// This runs if the input wasn't a valid regex pattern OR if the try-catch failed.
return cellValueString.includes(filterValueString.toLowerCase());
}

export function formatSize(size = 0): string {
const oneGB = 1024 * 1024 * 1024,
oneMB = 1024 * 1024,
oneKB = 1024;

if (size >= oneGB) {
return `${(size / oneGB).toFixed(2)} GB`;
} else if (size >= oneMB) {
return `${(size / oneMB).toFixed(2)} MB`;
} else if (size >= oneKB) {
return `${(size / oneKB).toFixed(2)} KB`;
}
return `${size} bytes`;
}
2 changes: 1 addition & 1 deletion extensions/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@
"@cornerstonejs/calculate-suv": "^1.1.0",
"lodash.get": "^4.4.2",
"lodash.uniqby": "^4.7.0",
"cod-dicomweb-server": "^1.3.11"
"cod-dicomweb-server": "^1.3.16"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,11 @@ class CodDicomWebServerClient {
studyFound = this._findStudy(this._studiesMetadata, { StudyInstanceUID: studyInstanceUID });
}

const seriesUID = queryParams?.SeriesInstanceUID;
const seriesUIDs = seriesUID ? (Array.isArray(seriesUID) ? seriesUID : [seriesUID]) : [];
// In COD format, the DeidSeriesInstanceUID is used to identify series instead of SeriesInstanceUID.
const seriesFound = studyFound?.series.find(
({ deidSeriesInstanceUID }) => deidSeriesInstanceUID === queryParams?.SeriesInstanceUID
const seriesFound = studyFound?.series.find(({ deidSeriesInstanceUID }) =>
seriesUIDs.includes(deidSeriesInstanceUID)
);

return new Promise(resolve => {
Expand Down
2 changes: 1 addition & 1 deletion extensions/default/src/DicomWebDataSource/getCodImageId.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function getCodImageId({ instance, frame, config }) {
wadoRsImageId = getWADORSImageId(instance, config, frame);
}

if (config.useURLParams && !instance.imageId && instance.BucketPath) {
if (config.useURLParams && (!instance.imageId || frame) && instance.BucketPath) {
wadoRsImageId = wadoRsImageId.replace(
config.wadoRoot,
`${config.wadoRoot}/${instance.BucketPath}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function retrieveStudyMetadata(
throw new Error(`${moduleName}: Required 'StudyInstanceUID' parameter not provided.`);
}

const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}`;
const promiseId = `${dicomWebConfig.name}:${StudyInstanceUID}:${filters?.seriesInstanceUID?.toString() || 'NO-SERIES-FILTER'}`;

// Already waiting on result? Return cached promise
if (StudyMetaDataPromises.has(promiseId)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcM
countIcon: ds.countIcon,
messages: ds.messages,
StudyInstanceUID: ds.StudyInstanceUID,
DeidSeriesInstanceUID: ds.instance.DeidSeriesInstanceUID,
componentType,
imageSrc: thumbnailSrc || thumbnailImageSrcMap[displaySetInstanceUID],
dragData: {
Expand Down
4 changes: 2 additions & 2 deletions extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,11 +628,11 @@ const commandsModule = ({
setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0);
},

downloadSeriesFile: ({ displaySetInstanceUID }) => {
downloadSeriesFile: async ({ displaySetInstanceUID }) => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const codServer = internal.getWadoRsWebServer();
const deidSeriesInstanceUID = displaySet.instance.DeidSeriesInstanceUID;
const seriesDownloaded = codServer.downloadSeriesFile(deidSeriesInstanceUID);
const seriesDownloaded = await codServer.downloadSeriesFile(deidSeriesInstanceUID);

uiNotificationService.show({
title: 'Download Series file',
Expand Down
Loading