Describe Your Question
- I was trying to implement a functionality in OHIF of editing tags in the tag browser and sync that with DCM4CHEE, but currently encountering some issues, it would be great help if someone who has done the same can help me out.
- OHIF v3
What steps can we follow to reproduce the bug?
- it’s not a bug but rather a functionality.
- But still, I added an edit button, on click of which I’m changing the values of the tag to input element with the same value, after editing those values.
- After that I’m hitting the DCM4CHEE APIs, respectively for the tags, DCM offers multiple API to update the data for different levels, like patient, study and series. I’m unable to perfectly mould my API payload accordingly.
Below is how my DicomTagBrowser file currently look like -
import dcmjs from 'dcmjs';
import moment from 'moment';
import React, { useState, useMemo, useEffect } from 'react';
import { classes } from '@ohif/core';
import { InputRange, Select, Typography, InputFilterText, Button } from '@ohif/ui';
import debounce from 'lodash.debounce';
import DicomTagTable from './DicomTagTable';
import './DicomTagBrowser.css';
import {
editableTags, editableTagsPatientDemographics, editableTagsSeriesLevel, editableTagsStudy,
editableTagsSeriesLevelSend,
editableTagsPatientDemographicsSend,
editableTagsStudySend
} from '../editableTags';
const { ImageSet } = classes;
const { DicomMetaDictionary } = dcmjs.data;
const { nameMap } = DicomMetaDictionary;
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
// The column indices that are to be excluded during a filter of the table.
// At present the column indices are:
// 0: DICOM tag
// 1: VR
// 2: Keyword
// 3: Value
const excludedColumnIndicesForFilter: Set<number> = new Set([1]);
const [selectedDisplaySetInstanceUID, setSelectedDisplaySetInstanceUID] =
useState(displaySetInstanceUID);
const [instanceNumber, setInstanceNumber] = useState(1);
const [filterValue, setFilterValue] = useState('');
const [editTag, setEditTag] = useState(false);
const [changedTags, setChangedTags] = useState({});
const [changedTagsPatient, setChangedTagsPatient] = useState({});
const [changedTagsStudy, setChangedTagsStudy] = useState({});
const [changedTagsSeries, setChangedTagsSeries] = useState({});
const onSelectChange = value => {
setSelectedDisplaySetInstanceUID(value.value);
setInstanceNumber(1);
};
const activeDisplaySet = displaySets.find(
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
);
console.log("selectedDisplaySetInstanceUID", selectedDisplaySetInstanceUID, activeDisplaySet);
const isImageStack = _isImageStack(activeDisplaySet);
const showInstanceList = isImageStack && activeDisplaySet.images.length > 1;
const displaySetList = useMemo(() => {
displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber);
return displaySets.map(displaySet => {
const {
displaySetInstanceUID,
SeriesDate,
SeriesTime,
SeriesNumber,
SeriesDescription,
Modality,
} = displaySet;
/* Map to display representation */
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
const displayDate = date.format('ddd, MMM Do YYYY');
return {
value: displaySetInstanceUID,
label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
description: displayDate,
};
});
}, [displaySets]);
const rows = useMemo(() => {
let metadata;
if (isImageStack) {
metadata = activeDisplaySet.images[instanceNumber - 1];
} else {
metadata = activeDisplaySet.instance || activeDisplaySet;
}
const tags = getSortedTags(metadata);
return getFormattedRowsFromTags(tags, metadata);
}, [instanceNumber, selectedDisplaySetInstanceUID]);
const filteredRows = useMemo(() => {
if (!filterValue) {
return rows;
}
const filterValueLowerCase = filterValue.toLowerCase();
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)) {
return keepRow;
}
return keepRow || col.toLowerCase().includes(filterValueLowerCase);
}, false);
});
}, [rows, filterValue]);
const debouncedSetFilterValue = useMemo(() => {
return debounce(setFilterValue, 200);
}, []);
useEffect(() => {
return () => {
debouncedSetFilterValue?.cancel();
};
}, []);
const [isDarkMode, setIsDarkMode] = useState(localStorage.getItem('isLightMode') == 'true' ? false : true)
useEffect(() => {
setIsDarkMode(!isDarkMode);
}, [localStorage.getItem('isLightMode')]);
const handleSaveTags = async () => {
const updatedTags = rows.map(row => {
if (changedTags[row[0]]) {
return {
tag: row[0],
value: changedTags[row[0]],
};
}
return null;
}).filter(tag => tag !== null);
// Call your API or method to save updatedTags
console.log('rrrrrrrrrrrrrrrrrrrrrrrrrrr', updatedTags);
const tagsOfInterest = ['(0010,0020)', '(0010,0021)', '(0020,000E)'];
dummy
// Filter rows to get only those that match the tags of interest
const filteredRows = rows.filter(row => tagsOfInterest.includes(row[0]));
// Extract values
const values = filteredRows.map(row => row[3]);
console.log('values', values[0] /*for patientid and value[1] for issuerofPatientId value[1], 2 for seriesInstancedid */);
const currentUrl = window.location.href;
const currUrl = new URL(currentUrl);
const searchParams = new URLSearchParams(currUrl.search);
const studyInstanceUID = searchParams.get('StudyInstanceUIDs');
// console.log('dddddd', transformDataConditionally(dummy));
const isTagInList = (tag, list) => list.some(item => item.tag === tag);
// Study Level Tags
const studyLevelTags = editableTagsStudy;
const studyLevelChanges = updatedTags.filter(tag => isTagInList(tag.tag, studyLevelTags));
console.log("🚀 ~ handleSaveTags ~ updatedTags:", updatedTags)
console.log("🚀 ~ handleSaveTags ~ transformDataConditionally(dummy):", transformDataConditionally(dummy))
console.log("🚀 ~ handleSaveTags ~ dummy:", dummy)
console.log("🚀 ~ handleSaveTags ~ changedTagsStudy:", changedTagsStudy)
transformDataConditionally(dummy);
if (studyLevelChanges.length > 0) {
//if study level tag
const fetchedData = await fetch(`http://localhost:8081/dcm4chee-arc/aets/DCM4CHEE/rs/studies/${studyInstanceUID}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(changedTagsStudy),
});
}
// If patient demographics level tag
const patientDemographicsTags = editableTagsPatientDemographics;
const patientDemographicsChanges = updatedTags.filter(tag => isTagInList(tag.tag, patientDemographicsTags));
if (patientDemographicsChanges.length > 0) {
const patientID = values[0]; // Replace with the actual patient ID if dynamic
const patientIdentifier = values[1]; // Replace with the actual identifier if dynamic
// Define the URL for the API call
const url = `http://localhost:8081/dcm4chee-arc/aets/DCM4CHEE/rs/patients/${patientID}^^^${patientIdentifier}`;
// Transform the data to match the API requirements
//Make the API call to update the study data
const fetchedDataStudy = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(changedTagsPatient),
});
}
// if series level tag
// Define the URL for the API call
const seriesLevelTags = editableTagsSeriesLevel;
const seriesLevelChanges = updatedTags.filter(tag => isTagInList(tag.tag, seriesLevelTags));
if (seriesLevelChanges.length > 0) {
const seriesInstanceUid = values[2];
const urlSeries = `http://localhost:8081/dcm4chee-arc/aets/DCM4CHEE/rs/studies/${studyInstanceUID}/series/${seriesInstanceUid}`;
const fetchedDataSeries = await fetch(urlSeries, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(changedTagsSeries),
});
}
setChangedTags({});
setEditTag(false);
}
const transformDataConditionally = (rows) => {
const updatedTags = rows.map(row => {
if (changedTags[row[0]]) {
return {
tag: row[0],
value: changedTags[row[0]],
};
}
return null;
}).filter(tag => tag !== null);
const isTagInList = (tag, list) => list.some(item => item.tag === tag);
// Study Level Tags
const studyLevelTags = editableTagsStudy;
const studyLevelChanges = updatedTags.filter(tag => isTagInList(tag.tag, studyLevelTags));
if (studyLevelChanges.length > 0) {
// Convert editableTags array into a Set for fast lookup
let studyEditableTagsSet = new Set(editableTagsStudySend.map(tag => tag.tag.replace(/[()]/g, '').replace(',', '')));
let temp = returnSpecificTagList(studyEditableTagsSet, null);
setChangedTagsStudy(temp);
}
const seriesLevelTags = editableTagsSeriesLevel;
const seriesLevelChanges = updatedTags.filter(tag => isTagInList(tag.tag, seriesLevelTags));
if (seriesLevelChanges.length > 0) {
let seriesEditableTagsSet = new Set(editableTagsSeriesLevelSend.map(tag => tag.tag.replace(/[()]/g, '').replace(',', '')));
let temp = returnSpecificTagList(seriesEditableTagsSet, null);
setChangedTagsSeries(temp);
}
const patientDemographicsTags = editableTagsPatientDemographics;
const patientDemographicsChanges = updatedTags.filter(tag => isTagInList(tag.tag, patientDemographicsTags));
if (patientDemographicsChanges.length > 0) {
let patientEditableTags = new Set(editableTagsPatientDemographicsSend.map(tag => tag.tag.replace(/[()]/g, '').replace(',', '')));
let temp = returnSpecificTagList(patientEditableTags, patientDemographicsChanges);
setChangedTagsPatient(temp);
}
};
const returnSpecificTagList = (editableTagsSet, patientDemographicsChanges) => {
const result = {};
rows.forEach(([tag, vr, ...values]) => {
const formattedTag = tag.replace(/[()]/g, '').replace(',', '');
// Check if the tag is in the editableTags list
if (editableTagsSet.has(formattedTag)) {
let value = values[1];
// Special handling for Patient Name (PN) field in Patient Demographics
if (formattedTag === '00100010' && vr === 'PN' && patientDemographicsChanges != null && patientDemographicsChanges.length > 0) {
value = value.split('^').map((nameComponent) => nameComponent.trim()).join('^');
result[formattedTag] = {
vr: vr,
Value: [
{
Alphabetic: value
}
]
};
} else {
result[formattedTag] = {
vr: vr,
Value: [value]
};
}
}
});
return result;
}
const transformData = (rows) => {
// Convert editableTags array into a Set for fast lookup
const editableTagsSet = new Set(editableTags.map(tag => tag.tag.replace(/[()]/g, '').replace(',', '')));
const result = {};
rows.forEach(([tag, vr, ...values]) => {
const formattedTag = tag.replace(/[()]/g, '').replace(',', '');
// Check if the tag is in the editableTags list
if (editableTagsSet.has(formattedTag)) {
result[formattedTag] = {
vr: vr,
Value: [values[1]]
};
}
});
return result;
};
const [dummy, setDummy] = useState(transformData(filteredRows));
//console.log(JSON.stringify(dummy, null, 4));
return (
<div className="dicom-tag-browser-content">
<div className="mb-6 flex flex-row items-center pl-1">
<div className="flex w-1/2 flex-row items-center">
<Typography
variant="subtitle"
className={isDarkMode ? "text-black mr-4" : "mr-4"}
>
Series
</Typography>
<div className="mr-8 grow">
<Select
id="display-set-selector"
isClearable={false}
onChange={onSelectChange}
options={displaySetList}
value={displaySetList.find(ds => ds.value === selectedDisplaySetInstanceUID)}
className="text-white"
/>
</div>
</div>
<div className="flex w-1/2 flex-row items-center">
{showInstanceList && (
<Typography
variant="subtitle"
className={isDarkMode ? "text-black mr-4" : "mr-4"}
>
Instance Number
</Typography>
)}
{showInstanceList && (
<div className="grow">
<InputRange
value={instanceNumber}
key={selectedDisplaySetInstanceUID}
onChange={value => {
setInstanceNumber(parseInt(value));
}}
minValue={1}
maxValue={activeDisplaySet.images.length}
step={1}
inputClassName="w-full"
labelPosition="left"
trackColor={'#3a3f99'}
/>
</div>
)}
</div>
</div>
<div className="h-1 w-full bg-black"></div>
<div className="my-3 flex flex-row justify-between">
<InputFilterText
className="mr-8 block"
placeholder="Search metadata..."
onDebounceChange={setFilterValue}
/>
<div>
<Button onClick={() => { setEditTag(!editTag) }} className='mr-3' disabled={filterValue !== ''}>Edit Tags</Button>
<Button onClick={handleSaveTags} className='bg-primaryGreen-foreground hover:bg-primaryGreen' disabled={filterValue !== ''}>Save</Button>
</div>
</div>
<DicomTagTable rows={filteredRows} editTag={editTag} handleSaveTags={handleSaveTags} dummy={dummy} setDummy={setDummy} setChangedTags={setChangedTags} />
</div>
);
};
function getFormattedRowsFromTags(tags, metadata) {
const rows = [];
tags.forEach(tagInfo => {
if (tagInfo.vr === 'SQ') {
rows.push([`${tagInfo.tagIndent}${tagInfo.tag}`, tagInfo.vr, tagInfo.keyword, '']);
const { values } = tagInfo;
values.forEach((item, index) => {
const formatedRowsFromTags = getFormattedRowsFromTags(item, metadata);
rows.push([`${item[0].tagIndent}(FFFE,E000)`, '', `Item #${index}`, '']);
rows.push(...formatedRowsFromTags);
});
} else {
if (tagInfo.vr === 'xs') {
try {
const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
const originalTagInfo = metadata[tag];
tagInfo.vr = originalTagInfo.vr;
} catch (error) {
console.error(`Failed to parse value representation for tag '${tagInfo.keyword}'`);
}
}
rows.push([`${tagInfo.tagIndent}${tagInfo.tag}`, tagInfo.vr, tagInfo.keyword, tagInfo.value]);
}
});
return rows;
}
function getSortedTags(metadata) {
const tagList = getRows(metadata);
// Sort top level tags, sequence groups are sorted when created.
_sortTagList(tagList);
return tagList;
}
function getRows(metadata, depth = 0) {
// Tag, Type, Value, Keyword
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 = [];
for (let i = 0; i < keywords.length; i++) {
let keyword = keywords[i];
if (keyword === '_vrMap') {
continue;
}
const tagInfo = nameMap[keyword];
let value = metadata[keyword];
if (tagInfo && tagInfo.vr === 'SQ') {
const sequenceAsArray = toArray(value);
// Push line defining the sequence
const sequence = {
tag: tagInfo.tag,
tagIndent,
vr: tagInfo.vr,
keyword,
values: [],
};
rows.push(sequence);
if (value === null) {
// Type 2 Sequence
continue;
}
sequenceAsArray.forEach(item => {
const sequenceRows = getRows(item, depth + 1);
if (sequenceRows.length) {
// Sort the sequence group.
_sortTagList(sequenceRows);
sequence.values.push(sequenceRows);
}
});
continue;
}
if (Array.isArray(value)) {
if (value.length > 0 && typeof value[0] != 'object') {
value = value.join('\\');
}
}
if (typeof value === 'number') {
value = value.toString();
}
if (typeof value !== 'string') {
if (value === null) {
value = ' ';
} else {
if (typeof value === 'object') {
if (value.InlineBinary) {
value = 'Inline Binary';
} else if (value.BulkDataURI) {
value = `Bulk Data URI`; //: ${value.BulkDataURI}`;
} else if (value.Alphabetic) {
value = value.Alphabetic;
} else {
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
console.warn(value);
value = ' ';
}
} else {
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
value = ' ';
}
}
}
// tag / vr/ keyword/ value
// Remove retired tags
keyword = keyword.replace('RETIRED_', '');
if (tagInfo) {
rows.push({
tag: tagInfo.tag,
tagIndent,
vr: tagInfo.vr,
keyword,
value,
});
} else {
// skip properties without hex tag numbers
const regex = /[0-9A-Fa-f]{6}/g;
if (keyword.match(regex)) {
const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`;
rows.push({
tag,
tagIndent,
vr: '',
keyword: 'Private Tag',
value,
});
}
}
}
return rows;
}
function _isImageStack(displaySet) {
return displaySet instanceof ImageSet;
}
function toArray(objectOrArray) {
return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray];
}
function _sortTagList(tagList) {
tagList.sort((a, b) => {
if (a.tag < b.tag) {
return -1;
}
return 1;
});
}
export default DicomTagBrowser;