feat: improve dicom tag browser with nested rows (#4451)
This commit is contained in:
parent
8b969eb1e9
commit
0b5836ca1a
@ -1,30 +1,44 @@
|
|||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { classes } from '@ohif/core';
|
import { classes, Types } from '@ohif/core';
|
||||||
import { InputFilterText } from '@ohif/ui';
|
import { InputFilterText } from '@ohif/ui';
|
||||||
import debounce from 'lodash.debounce';
|
|
||||||
import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
|
import { Select, SelectTrigger, SelectContent, SelectItem, Slider } from '@ohif/ui-next';
|
||||||
|
|
||||||
import DicomTagTable from './DicomTagTable';
|
import DicomTagTable from './DicomTagTable';
|
||||||
import './DicomTagBrowser.css';
|
import './DicomTagBrowser.css';
|
||||||
|
|
||||||
|
export type Row = {
|
||||||
|
uid: string;
|
||||||
|
tag: string;
|
||||||
|
valueRepresentation: string;
|
||||||
|
keyword: string;
|
||||||
|
value: string;
|
||||||
|
isVisible: boolean;
|
||||||
|
depth: number;
|
||||||
|
parents?: string[];
|
||||||
|
children?: string[];
|
||||||
|
areChildrenVisible?: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let rowCounter = 0;
|
||||||
|
const generateRowId = () => `row_${++rowCounter}`;
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
const { ImageSet } = classes;
|
||||||
const { DicomMetaDictionary } = dcmjs.data;
|
const { DicomMetaDictionary } = dcmjs.data;
|
||||||
const { nameMap } = DicomMetaDictionary;
|
const { nameMap } = DicomMetaDictionary;
|
||||||
|
|
||||||
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
const DicomTagBrowser = ({
|
||||||
// The column indices that are to be excluded during a filter of the table.
|
displaySets,
|
||||||
// At present the column indices are:
|
displaySetInstanceUID,
|
||||||
// 0: DICOM tag
|
}: {
|
||||||
// 1: VR
|
displaySets: Types.DisplaySet[];
|
||||||
// 2: Keyword
|
displaySetInstanceUID: string;
|
||||||
// 3: Value
|
}) => {
|
||||||
const excludedColumnIndicesForFilter: Set<number> = new Set([1]);
|
|
||||||
|
|
||||||
const [selectedDisplaySetInstanceUID, setSelectedDisplaySetInstanceUID] =
|
const [selectedDisplaySetInstanceUID, setSelectedDisplaySetInstanceUID] =
|
||||||
useState(displaySetInstanceUID);
|
useState(displaySetInstanceUID);
|
||||||
const [instanceNumber, setInstanceNumber] = useState(1);
|
const [instanceNumber, setInstanceNumber] = useState(1);
|
||||||
|
const [shouldShowInstanceList, setShouldShowInstanceList] = useState(false);
|
||||||
const [filterValue, setFilterValue] = useState('');
|
const [filterValue, setFilterValue] = useState('');
|
||||||
|
|
||||||
const onSelectChange = value => {
|
const onSelectChange = value => {
|
||||||
@ -36,9 +50,6 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
|
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const isImageStack = _isImageStack(activeDisplaySet);
|
|
||||||
const showInstanceList = isImageStack && activeDisplaySet.images.length > 1;
|
|
||||||
|
|
||||||
const displaySetList = useMemo(() => {
|
const displaySetList = useMemo(() => {
|
||||||
displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber);
|
displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber);
|
||||||
return displaySets.map(displaySet => {
|
return displaySets.map(displaySet => {
|
||||||
@ -64,48 +75,53 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
});
|
});
|
||||||
}, [displaySets]);
|
}, [displaySets]);
|
||||||
|
|
||||||
|
const getMetadata = useCallback(
|
||||||
|
isImageStack => {
|
||||||
|
if (isImageStack) {
|
||||||
|
return activeDisplaySet.images[instanceNumber - 1];
|
||||||
|
}
|
||||||
|
return activeDisplaySet.instance || activeDisplaySet;
|
||||||
|
},
|
||||||
|
[activeDisplaySet, instanceNumber]
|
||||||
|
);
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
let metadata;
|
const isImageStack = activeDisplaySet instanceof ImageSet;
|
||||||
if (isImageStack) {
|
const metadata = getMetadata(isImageStack);
|
||||||
metadata = activeDisplaySet.images[instanceNumber - 1];
|
|
||||||
} else {
|
setShouldShowInstanceList(isImageStack && activeDisplaySet.images.length > 1);
|
||||||
metadata = activeDisplaySet.instance || activeDisplaySet;
|
|
||||||
}
|
|
||||||
const tags = getSortedTags(metadata);
|
const tags = getSortedTags(metadata);
|
||||||
return getFormattedRowsFromTags(tags, metadata);
|
const rows = getFormattedRowsFromTags({ tags, metadata, depth: 0 });
|
||||||
}, [instanceNumber, selectedDisplaySetInstanceUID]);
|
return rows;
|
||||||
|
}, [getMetadata, activeDisplaySet]);
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
if (!filterValue) {
|
if (!filterValue) {
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterValueLowerCase = filterValue.toLowerCase();
|
const matchedRowIds = new Set();
|
||||||
return rows.filter(row => {
|
|
||||||
return row.reduce((keepRow, col, colIndex) => {
|
|
||||||
if (keepRow) {
|
|
||||||
// We are already keeping the row, why do more work so return now.
|
|
||||||
return keepRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludedColumnIndicesForFilter.has(colIndex)) {
|
const propertiesToCheck = ['tag', 'valueRepresentation', 'keyword', 'value'];
|
||||||
return keepRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
return keepRow || col.toLowerCase().includes(filterValueLowerCase);
|
const setIsMatched = row => {
|
||||||
}, false);
|
const isDirectMatch = propertiesToCheck.some(propertyName =>
|
||||||
});
|
row[propertyName]?.toLowerCase().includes(filterValueLowerCase)
|
||||||
}, [rows, filterValue]);
|
);
|
||||||
|
|
||||||
const debouncedSetFilterValue = useMemo(() => {
|
if (!isDirectMatch) {
|
||||||
return debounce(setFilterValue, 200);
|
return;
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
matchedRowIds.add(row.uid);
|
||||||
return () => {
|
|
||||||
debouncedSetFilterValue?.cancel();
|
[...(row.parents ?? []), ...(row.children ?? [])].forEach(uid => matchedRowIds.add(uid));
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
const filterValueLowerCase = filterValue.toLowerCase();
|
||||||
|
rows.forEach(setIsMatched);
|
||||||
|
return rows.filter(row => matchedRowIds.has(row.uid));
|
||||||
|
}, [rows, filterValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dicom-tag-browser-content bg-muted">
|
<div className="dicom-tag-browser-content bg-muted">
|
||||||
@ -136,10 +152,10 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{showInstanceList && (
|
{shouldShowInstanceList && (
|
||||||
<div className="mx-auto flex w-1/5 flex-col">
|
<div className="mx-auto flex w-1/5 flex-col">
|
||||||
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
<span className="text-muted-foreground flex h-6 items-center text-xs">
|
||||||
Instance Number ({instanceNumber} of {activeDisplaySet.images.length})
|
Instance Number ({instanceNumber} of {activeDisplaySet?.images?.length})
|
||||||
</span>
|
</span>
|
||||||
<Slider
|
<Slider
|
||||||
value={[instanceNumber]}
|
value={[instanceNumber]}
|
||||||
@ -147,7 +163,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
setInstanceNumber(value);
|
setInstanceNumber(value);
|
||||||
}}
|
}}
|
||||||
min={1}
|
min={1}
|
||||||
max={activeDisplaySet.images.length}
|
max={activeDisplaySet?.images?.length}
|
||||||
step={1}
|
step={1}
|
||||||
className="pt-4"
|
className="pt-4"
|
||||||
/>
|
/>
|
||||||
@ -169,22 +185,33 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFormattedRowsFromTags(tags, metadata) {
|
function getFormattedRowsFromTags({ tags, metadata, depth, parents }) {
|
||||||
const rows = [];
|
const rows: Row[] = [];
|
||||||
|
|
||||||
tags.forEach(tagInfo => {
|
tags.forEach(tagInfo => {
|
||||||
|
const uid = generateRowId();
|
||||||
if (tagInfo.vr === 'SQ') {
|
if (tagInfo.vr === 'SQ') {
|
||||||
rows.push([`${tagInfo.tagIndent}${tagInfo.tag}`, tagInfo.vr, tagInfo.keyword, '']);
|
const children = tagInfo.values.flatMap(value =>
|
||||||
|
getFormattedRowsFromTags({
|
||||||
const { values } = tagInfo;
|
tags: value,
|
||||||
|
metadata,
|
||||||
values.forEach((item, index) => {
|
depth: depth + 1,
|
||||||
const formatedRowsFromTags = getFormattedRowsFromTags(item, metadata);
|
parents: parents ? [...parents, uid] : [uid],
|
||||||
|
})
|
||||||
rows.push([`${item[0].tagIndent}(FFFE,E000)`, '', `Item #${index}`, '']);
|
);
|
||||||
|
const row: Row = {
|
||||||
rows.push(...formatedRowsFromTags);
|
uid,
|
||||||
});
|
tag: tagInfo.tag,
|
||||||
|
valueRepresentation: tagInfo.vr,
|
||||||
|
keyword: tagInfo.keyword,
|
||||||
|
value: '',
|
||||||
|
depth,
|
||||||
|
isVisible: true,
|
||||||
|
areChildrenVisible: true,
|
||||||
|
children: children.map(child => child.uid),
|
||||||
|
parents,
|
||||||
|
};
|
||||||
|
rows.push(row, ...children);
|
||||||
} else {
|
} else {
|
||||||
if (tagInfo.vr === 'xs') {
|
if (tagInfo.vr === 'xs') {
|
||||||
try {
|
try {
|
||||||
@ -192,10 +219,20 @@ function getFormattedRowsFromTags(tags, metadata) {
|
|||||||
const originalTagInfo = metadata[tag];
|
const originalTagInfo = metadata[tag];
|
||||||
tagInfo.vr = originalTagInfo.vr;
|
tagInfo.vr = originalTagInfo.vr;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to parse value representation for tag '${tagInfo.keyword}'`);
|
console.warn(`Failed to parse value representation for tag '${tagInfo.keyword}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows.push([`${tagInfo.tagIndent}${tagInfo.tag}`, tagInfo.vr, tagInfo.keyword, tagInfo.value]);
|
const row: Row = {
|
||||||
|
uid,
|
||||||
|
tag: tagInfo.tag,
|
||||||
|
valueRepresentation: tagInfo.vr,
|
||||||
|
keyword: tagInfo.keyword,
|
||||||
|
value: tagInfo.value,
|
||||||
|
depth,
|
||||||
|
isVisible: true,
|
||||||
|
parents,
|
||||||
|
};
|
||||||
|
rows.push(row);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -216,16 +253,6 @@ function getRows(metadata, depth = 0) {
|
|||||||
|
|
||||||
const keywords = Object.keys(metadata);
|
const keywords = Object.keys(metadata);
|
||||||
|
|
||||||
let tagIndent = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < depth; i++) {
|
|
||||||
tagIndent += '>';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth > 0) {
|
|
||||||
tagIndent += ' '; // If indented, add a space after the indents.
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (let i = 0; i < keywords.length; i++) {
|
for (let i = 0; i < keywords.length; i++) {
|
||||||
let keyword = keywords[i];
|
let keyword = keywords[i];
|
||||||
@ -245,7 +272,6 @@ function getRows(metadata, depth = 0) {
|
|||||||
|
|
||||||
const sequence = {
|
const sequence = {
|
||||||
tag: tagInfo.tag,
|
tag: tagInfo.tag,
|
||||||
tagIndent,
|
|
||||||
vr: tagInfo.vr,
|
vr: tagInfo.vr,
|
||||||
keyword,
|
keyword,
|
||||||
values: [],
|
values: [],
|
||||||
@ -311,7 +337,6 @@ function getRows(metadata, depth = 0) {
|
|||||||
if (tagInfo) {
|
if (tagInfo) {
|
||||||
rows.push({
|
rows.push({
|
||||||
tag: tagInfo.tag,
|
tag: tagInfo.tag,
|
||||||
tagIndent,
|
|
||||||
vr: tagInfo.vr,
|
vr: tagInfo.vr,
|
||||||
keyword,
|
keyword,
|
||||||
value,
|
value,
|
||||||
@ -323,7 +348,6 @@ function getRows(metadata, depth = 0) {
|
|||||||
const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`;
|
const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`;
|
||||||
rows.push({
|
rows.push({
|
||||||
tag,
|
tag,
|
||||||
tagIndent,
|
|
||||||
vr: '',
|
vr: '',
|
||||||
keyword: 'Private Tag',
|
keyword: 'Private Tag',
|
||||||
value,
|
value,
|
||||||
@ -335,10 +359,6 @@ function getRows(metadata, depth = 0) {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isImageStack(displaySet) {
|
|
||||||
return displaySet instanceof ImageSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toArray(objectOrArray) {
|
function toArray(objectOrArray) {
|
||||||
return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray];
|
return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
import { VariableSizeList as List } from 'react-window';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import { Row } from './DicomTagBrowser';
|
||||||
|
import { Icons } from '@ohif/ui-next';
|
||||||
|
|
||||||
const lineHeightPx = 20;
|
const lineHeightPx = 20;
|
||||||
const lineHeightClassName = `leading-[${lineHeightPx}px]`;
|
const lineHeightClassName = `leading-[${lineHeightPx}px]`;
|
||||||
@ -12,6 +14,52 @@ const rowStyle = {
|
|||||||
borderBottomWidth: `${rowBottomBorderPx}px`,
|
borderBottomWidth: `${rowBottomBorderPx}px`,
|
||||||
...rowVerticalPaddingStyle,
|
...rowVerticalPaddingStyle,
|
||||||
};
|
};
|
||||||
|
const indentationPadding = 8;
|
||||||
|
|
||||||
|
const RowComponent = ({
|
||||||
|
row,
|
||||||
|
style,
|
||||||
|
keyPrefix,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
row: Row;
|
||||||
|
style: any;
|
||||||
|
keyPrefix: string;
|
||||||
|
onToggle?: (areChildrenVisible: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
onToggle(!row.areChildrenVisible);
|
||||||
|
}, [row.areChildrenVisible, onToggle]);
|
||||||
|
|
||||||
|
const hasChildren = row.children && row.children.length > 0;
|
||||||
|
const isChildOrParent = hasChildren || row.depth > 0;
|
||||||
|
const padding = indentationPadding * (1 + 2 * row.depth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ ...style, ...rowStyle }}
|
||||||
|
className={classNames(
|
||||||
|
'hover:bg-secondary-main border-secondary-light flex w-full flex-row items-center break-all bg-black text-base transition duration-300',
|
||||||
|
lineHeightClassName
|
||||||
|
)}
|
||||||
|
key={keyPrefix}
|
||||||
|
>
|
||||||
|
{isChildOrParent && (
|
||||||
|
<div style={{ paddingLeft: `${padding}px`, opacity: onToggle ? 1 : 0 }}>
|
||||||
|
{row.areChildrenVisible ? (
|
||||||
|
<Icons.ChevronDown onClick={handleToggle} />
|
||||||
|
) : (
|
||||||
|
<Icons.ChevronRight onClick={handleToggle} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="w-4/24 px-3">{row.tag}</div>
|
||||||
|
<div className="w-2/24 px-3">{row.valueRepresentation}</div>
|
||||||
|
<div className="w-6/24 px-3">{row.keyword}</div>
|
||||||
|
<div className="w-5/24 grow px-3">{row.value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) {
|
function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) {
|
||||||
return (
|
return (
|
||||||
@ -56,8 +104,7 @@ function ColumnHeaders({ tagRef, vrRef, keywordRef, valueRef }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function DicomTagTable({ rows }: { rows: Row[] }) {
|
||||||
function DicomTagTable({ rows }) {
|
|
||||||
const listRef = useRef();
|
const listRef = useRef();
|
||||||
const canvasRef = useRef();
|
const canvasRef = useRef();
|
||||||
|
|
||||||
@ -65,6 +112,7 @@ function DicomTagTable({ rows }) {
|
|||||||
const [vrHeaderElem, setVrHeaderElem] = useState(null);
|
const [vrHeaderElem, setVrHeaderElem] = useState(null);
|
||||||
const [keywordHeaderElem, setKeywordHeaderElem] = useState(null);
|
const [keywordHeaderElem, setKeywordHeaderElem] = useState(null);
|
||||||
const [valueHeaderElem, setValueHeaderElem] = useState(null);
|
const [valueHeaderElem, setValueHeaderElem] = useState(null);
|
||||||
|
const [internalRows, setInternalRows] = useState(rows);
|
||||||
|
|
||||||
// Here the refs are inturn stored in state to trigger a render of the table.
|
// Here the refs are inturn stored in state to trigger a render of the table.
|
||||||
// This virtualized table does NOT render until the header is rendered because the header column widths are used to determine the row heights in the table.
|
// This virtualized table does NOT render until the header is rendered because the header column widths are used to determine the row heights in the table.
|
||||||
@ -90,6 +138,14 @@ function DicomTagTable({ rows }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInternalRows(rows);
|
||||||
|
}, [rows]);
|
||||||
|
|
||||||
|
const visibleRows = useMemo(() => {
|
||||||
|
return internalRows.filter(row => row.isVisible);
|
||||||
|
}, [internalRows]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When new rows are set, scroll to the top and reset the virtualization.
|
* When new rows are set, scroll to the top and reset the virtualization.
|
||||||
*/
|
*/
|
||||||
@ -116,42 +172,8 @@ function DicomTagTable({ rows }) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const Row = useCallback(
|
const getOneRowHeight = useCallback(
|
||||||
({ index, style }) => {
|
row => {
|
||||||
const row = rows[index];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ ...style, ...rowStyle }}
|
|
||||||
className={classNames(
|
|
||||||
'hover:bg-secondary-main border-secondary-light flex w-full flex-row items-center break-all bg-black text-base transition duration-300',
|
|
||||||
lineHeightClassName
|
|
||||||
)}
|
|
||||||
key={`DICOMTagRow-${index}`}
|
|
||||||
>
|
|
||||||
<div className="w-4/24 px-3">{row[0]}</div>
|
|
||||||
<div className="w-2/24 px-3">{row[1]}</div>
|
|
||||||
<div className="w-6/24 px-3">{row[2]}</div>
|
|
||||||
<div className="w-5/24 grow px-3">{row[3]}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[rows]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whenever any one of the column headers is set, then the header is rendered.
|
|
||||||
* Here we chose the tag header.
|
|
||||||
*/
|
|
||||||
const isHeaderRendered = useCallback(() => tagHeaderElem !== null, [tagHeaderElem]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the item/row size. We use the header column widths to calculate the various row heights.
|
|
||||||
* @param index the row index
|
|
||||||
* @returns the row height
|
|
||||||
*/
|
|
||||||
const getItemSize = useCallback(
|
|
||||||
index => {
|
|
||||||
const headerWidths = [
|
const headerWidths = [
|
||||||
tagHeaderElem.offsetWidth,
|
tagHeaderElem.offsetWidth,
|
||||||
vrHeaderElem.offsetWidth,
|
vrHeaderElem.offsetWidth,
|
||||||
@ -162,17 +184,79 @@ function DicomTagTable({ rows }) {
|
|||||||
const context = canvasRef.current.getContext('2d');
|
const context = canvasRef.current.getContext('2d');
|
||||||
context.font = getComputedStyle(canvasRef.current).font;
|
context.font = getComputedStyle(canvasRef.current).font;
|
||||||
|
|
||||||
return rows[index]
|
const propertiesToCheck = ['tag', 'valueRepresentation', 'keyword', 'value'];
|
||||||
.map((colText, index) => {
|
|
||||||
|
return Object.entries(row)
|
||||||
|
.filter(([key]) => propertiesToCheck.includes(key))
|
||||||
|
.map(([, colText], index) => {
|
||||||
const colOneLineWidth = context.measureText(colText).width;
|
const colOneLineWidth = context.measureText(colText).width;
|
||||||
const numLines = Math.ceil(colOneLineWidth / headerWidths[index]);
|
const numLines = Math.ceil(colOneLineWidth / headerWidths[index]);
|
||||||
return numLines * lineHeightPx + 2 * rowVerticalPaddingPx + rowBottomBorderPx;
|
return numLines * lineHeightPx + 2 * rowVerticalPaddingPx + rowBottomBorderPx;
|
||||||
})
|
})
|
||||||
.reduce((maxHeight, colHeight) => Math.max(maxHeight, colHeight));
|
.reduce((maxHeight, colHeight) => Math.max(maxHeight, colHeight), 0);
|
||||||
},
|
},
|
||||||
[rows, keywordHeaderElem, tagHeaderElem, valueHeaderElem, vrHeaderElem]
|
[keywordHeaderElem, tagHeaderElem, valueHeaderElem, vrHeaderElem]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the item/row size. We use the header column widths to calculate the various row heights.
|
||||||
|
* @param index the row index
|
||||||
|
* @returns the row height
|
||||||
|
*/
|
||||||
|
const getItemSize = useCallback(
|
||||||
|
rows => index => {
|
||||||
|
const row = rows[index];
|
||||||
|
const height = getOneRowHeight(row);
|
||||||
|
return height;
|
||||||
|
},
|
||||||
|
[getOneRowHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggle = useCallback(
|
||||||
|
sourceRow => {
|
||||||
|
if (!sourceRow.children) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return areChildrenVisible => {
|
||||||
|
const newInternalRows = internalRows.map(internalRow => {
|
||||||
|
if (sourceRow.uid === internalRow.uid) {
|
||||||
|
return { ...internalRow, areChildrenVisible };
|
||||||
|
}
|
||||||
|
if (sourceRow.children.includes(internalRow.uid)) {
|
||||||
|
return { ...internalRow, isVisible: areChildrenVisible, areChildrenVisible };
|
||||||
|
}
|
||||||
|
return internalRow;
|
||||||
|
});
|
||||||
|
setInternalRows(newInternalRows);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[internalRows]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRowComponent = useCallback(
|
||||||
|
({ rows }: { rows: Row[] }) =>
|
||||||
|
function RowList({ index, style }) {
|
||||||
|
const row = useMemo(() => rows[index], [index]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowComponent
|
||||||
|
style={style}
|
||||||
|
row={row}
|
||||||
|
keyPrefix={`DICOMTagRow-${index}`}
|
||||||
|
onToggle={onToggle(row)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[onToggle]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever any one of the column headers is set, then the header is rendered.
|
||||||
|
* Here we chose the tag header.
|
||||||
|
*/
|
||||||
|
const isHeaderRendered = useCallback(() => tagHeaderElem !== null, [tagHeaderElem]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<canvas
|
<canvas
|
||||||
@ -194,12 +278,12 @@ function DicomTagTable({ rows }) {
|
|||||||
<List
|
<List
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
height={500}
|
height={500}
|
||||||
itemCount={rows.length}
|
itemCount={visibleRows.length}
|
||||||
itemSize={getItemSize}
|
itemSize={getItemSize(visibleRows)}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
className="ohif-scrollbar"
|
className="ohif-scrollbar"
|
||||||
>
|
>
|
||||||
{Row}
|
{getRowComponent({ rows: visibleRows })}
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -207,4 +291,4 @@ function DicomTagTable({ rows }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DicomTagTable;
|
export default React.memo(DicomTagTable);
|
||||||
|
|||||||
@ -12,6 +12,9 @@ export type DisplaySet = {
|
|||||||
Modality?: string;
|
Modality?: string;
|
||||||
imageIds?: string[];
|
imageIds?: string[];
|
||||||
images?: unknown[];
|
images?: unknown[];
|
||||||
|
SeriesDate?: string;
|
||||||
|
SeriesTime?: string;
|
||||||
|
instance?: InstanceMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DisplaySetSeriesMetadataInvalidatedEvent = {
|
export type DisplaySetSeriesMetadataInvalidatedEvent = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user