diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
new file mode 100644
index 000000000..fe6a96096
--- /dev/null
+++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
@@ -0,0 +1,53 @@
+.dicom-tag-browser-table {
+ margin-right: auto;
+ margin-left: auto;
+}
+
+.dicom-tag-browser-table-wrapper {
+/* height: 500px;*/
+ /*overflow-y: scroll;*/
+ overflow-x: scroll;
+}
+
+.dicom-tag-browser-table tr {
+ padding-left: 10px;
+ padding-right: 10px;
+ color: #ffffff;
+ border-top: 1px solid #ddd;
+ white-space: nowrap;
+}
+
+.stick {
+ position: sticky;
+ overflow: clip;
+}
+
+.dicom-tag-browser-content {
+ overflow: hidden;
+ width: 100%;
+ padding-bottom: 50px;
+ /*height: 500px;*/
+}
+
+.dicom-tag-browser-instance-range .range {
+ height: 20px;
+}
+
+.dicom-tag-browser-instance-range {
+ padding: 20px 0 20px 0;
+}
+
+.dicom-tag-browser-table td.dicom-tag-browser-table-center {
+ text-align: center;
+}
+
+.dicom-tag-browser-table th {
+ padding-left: 10px;
+ padding-right: 10px;
+ text-align: center;
+ color: "#20A5D6";
+}
+
+.dicom-tag-browser-table th.dicom-tag-browser-table-left {
+ text-align: left;
+}
diff --git a/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
new file mode 100644
index 000000000..49155ca6a
--- /dev/null
+++ b/extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
@@ -0,0 +1,387 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { classes } from '@ohif/core';
+import Range from './Range';
+import DicomTagTable from './DicomTagTable';
+import dcmjs from 'dcmjs';
+import moment from 'moment';
+import './DicomTagBrowser.css';
+import { Select, Typography } from '@ohif/ui';
+
+const { ImageSet } = classes;
+const { DicomMetaDictionary } = dcmjs.data;
+const { nameMap } = DicomMetaDictionary;
+
+
+let instanceSelectList = null;
+let instanceSelectTitle = null;
+
+const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
+ const [
+ activeDisplaySetInstanceUID,
+ setActiveDisplaySetInstanceUID,
+ ] = useState(displaySetInstanceUID);
+ const [activeInstance, setActiveInstance] = useState(1);
+ const [tags, setTags] = useState([]);
+ const [meta, setMeta] = useState('');
+ const [instanceList, setInstanceList] = useState([]);
+ const [displaySetList, setDisplaySetList] = useState([]);
+ const [isImageStack, setIsImageStack] = useState(false);
+ const [selectedDisplaySetValue, setSelectedDisplaySetValue] = useState({}
+ );
+ const onSeriesSelect = value => {
+ console.log(value);
+ setActiveDisplaySetInstanceUID(value.value);
+ setActiveInstance(1);
+ setSelectedDisplaySetValue(value);
+ };
+
+ useEffect(() => {
+ var activeDisplaySet = displaySets.find(
+ ds => ds.displaySetInstanceUID === activeDisplaySetInstanceUID
+ );
+
+ displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber);
+
+ const newDisplaySetList = 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,
+ onClick: () => {
+ setActiveDisplaySetInstanceUID(displaySetInstanceUID);
+ setActiveInstance(1);
+
+ //instanceSelectList.props.children.props.value = 1;
+ },
+ };
+ });
+
+ let metadata;
+ if (!activeDisplaySet) {
+ if (!displaySets || displaySets.length == 0) {
+ return;
+ } else {
+ activeDisplaySet = displaySets[0];
+ setActiveDisplaySetInstanceUID(displaySets[0].displaySetInstanceUID);
+ setSelectedDisplaySetValue(newDisplaySetList[0]);
+ }
+
+ } else {
+
+ setSelectedDisplaySetValue(newDisplaySetList.find(
+ ds => ds.value === activeDisplaySetInstanceUID
+ ));
+ }
+ const isImageStack =
+ activeDisplaySet instanceof ImageSet; /*&&
+ activeDisplaySet.isSOPClassUIDSupported === true*/;
+
+ let instanceList;
+ if (isImageStack) {
+ const { images } = activeDisplaySet;
+ const image = images[activeInstance - 1];
+ instanceList = images.map((image, index) => {
+ // const metadata = image.getData().metadata;
+
+ const { InstanceNumber } = image;
+
+ return {
+ value: index,
+ title: `Instance Number: ${InstanceNumber}`,
+ description: '',
+ onClick: () => {
+ setActiveInstance(index);
+ },
+ };
+ });
+ metadata = image;
+ } else {
+ metadata = activeDisplaySet;
+ }
+
+
+ if (isImageStack) {
+ instanceSelectTitle = (
+
+ Instance Number
+
+ )
+ instanceSelectList = (
+
+
{value}
}
+ onChange={({ target }) => {
+ const instanceIndex = parseInt(target.value);
+ setActiveInstance(instanceIndex);
+ }}
+ />
+
+ );
+ } else {
+ instanceSelectList = null;
+ }
+ setTags(getSortedTags(metadata));
+ setMeta(metadata);
+ setInstanceList(instanceList);
+ setDisplaySetList(newDisplaySetList);
+ setIsImageStack(isImageStack);
+ }, [activeDisplaySetInstanceUID, activeInstance, displaySets]);
+
+ return (
+
+
+
+ Series
+
+ {instanceSelectTitle}
+
+
+
+
+ {instanceSelectList}
+
+
+
+
+
+
+ );
+};
+
+
+function getFormattedRowsFromTags(tags, meta) {
+ 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);
+
+ rows.push([
+ `${item[0].tagIndent}(FFFE,E000)`,
+ '',
+ `Item #${index}`,
+ '',
+ ]);
+
+ rows.push(...formatedRowsFromTags);
+ });
+ } else {
+ if (tagInfo.vr === 'xs') {
+ try {
+ /* const dataset = metadataProvider.getStudyDataset(
+ meta.StudyInstanceUID
+ );*/
+ // console.log(dataset);
+
+ // const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
+ // const originalTagInfo = dataset[tag];
+ // tagInfo.vr = originalTagInfo.vr;
+ } catch (error) {
+ console.error(
+ `Failed to parse value representation for tag '${tagInfo.keyword}'`
+ );
+ }
+ }
+ if (tagInfo.vr === 'PN') {
+ rows.push([
+ `${tagInfo.tagIndent}${tagInfo.tag}`,
+ tagInfo.vr,
+ tagInfo.keyword,
+ tagInfo.value
+ ]);
+ } else {
+
+ 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
+ var 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 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;
diff --git a/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx b/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
new file mode 100644
index 000000000..b63b4781a
--- /dev/null
+++ b/extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+function ColumnHeaders() {
+ return (
+
+
+
+
+
+
+
+
+
)
+}
+
+function DicomTagTable({ rows }) {
+
+ return ({ColumnHeaders()}
+
+
+
+
+ {rows.map((row, index) => {
+ const className = row.className ? row.className : null;
+
+ return (
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ );
+ })}
+
+
+ );
+}
+
+export default DicomTagTable;
diff --git a/extensions/default/src/DicomTagBrowser/Range.css b/extensions/default/src/DicomTagBrowser/Range.css
new file mode 100644
index 000000000..0e18f32d1
--- /dev/null
+++ b/extensions/default/src/DicomTagBrowser/Range.css
@@ -0,0 +1,65 @@
+.range {
+ margin: 0;
+ width: 100%;
+ -webkit-appearance: none;
+ background-color: transparent;
+}
+
+.range:focus {
+ outline: none;
+}
+
+.range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 2px;
+ cursor: pointer;
+ box-shadow: none;
+ background-color: #3C5D80;
+ border-radius: 0px;
+ border: 0px solid #3C5D80;
+}
+
+.range::-moz-range-track {
+ width: 100%;
+ height: 2px;
+ cursor: pointer;
+ box-shadow: none;
+ background-color: #3C5D80;
+ border-radius: 0px;
+ border: 0px solid #3C5D80;
+}
+
+.range::-webkit-slider-thumb {
+ box-shadow: none;
+ border: 0px solid #20A5D6;
+ box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25);
+ height: 21px;
+ width: 11px;
+ border-radius: 11px;
+ background: #20A5D6;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -10px;
+}
+
+.range::-moz-range-thumb {
+ box-shadow: none;
+ border: 0px solid #20A5D6;
+ box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25);
+ height: 21px;
+ width: 11px;
+ border-radius: 11px;
+ background: #20A5D6;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -10px;
+}
+
+.range::-moz-focus-outer {
+ border: 0;
+}
+
+.range-example {
+ background: black;
+ height: 2em;
+}
diff --git a/extensions/default/src/DicomTagBrowser/Range.tsx b/extensions/default/src/DicomTagBrowser/Range.tsx
new file mode 100644
index 000000000..f0462dcba
--- /dev/null
+++ b/extensions/default/src/DicomTagBrowser/Range.tsx
@@ -0,0 +1,57 @@
+import './Range.css';
+
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+function Range(props) {
+ const [value, setValue] = useState(props.value);
+ const handleChange = event => {
+ setValue(event.target.value);
+ if (props.onChange) props.onChange(event);
+ };
+
+
+ return (
+ <>
+
+ {props.showPercentage && {`${value}%`}}
+ {props.showValue && (
+
+ {props.valueRenderer
+ ? props.valueRenderer(props.value)
+ : props.value}
+
+ )}
+ >
+ );
+}
+
+Range.propTypes = {
+ value: PropTypes.number,
+ min: PropTypes.number.isRequired,
+ max: PropTypes.number.isRequired,
+ step: PropTypes.number,
+ id: PropTypes.string,
+ valueRenderer: PropTypes.func,
+ onChange: PropTypes.func,
+ showPercentage: PropTypes.bool,
+ showValue: PropTypes.bool,
+ ref: PropTypes.any,
+};
+
+Range.defaultProps = {
+ showPercentage: false,
+ showValue: false,
+};
+
+export default Range;
diff --git a/extensions/default/src/commandsModule.js b/extensions/default/src/commandsModule.js
index d5d9aca35..c66a0c788 100644
--- a/extensions/default/src/commandsModule.js
+++ b/extensions/default/src/commandsModule.js
@@ -1,8 +1,13 @@
+import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
+import React from 'react';
+
const commandsModule = ({ servicesManager, commandsManager }) => {
const {
MeasurementService,
HangingProtocolService,
UINotificationService,
+ ViewportGridService,
+ DisplaySetService
} = servicesManager.services;
const actions = {
@@ -23,6 +28,28 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
previousStage: () => {
HangingProtocolService.previousProtocolStage();
},
+ openDICOMTagViewer() {
+ const { activeViewportIndex, viewports } = ViewportGridService.getState();
+ const activeViewportSpecificData =
+ viewports[activeViewportIndex];
+ const {
+ displaySetInstanceUIDs,
+ } = activeViewportSpecificData;
+
+ const displaySets = DisplaySetService.activeDisplaySets;
+ const { UIModalService } = servicesManager.services;
+
+ const displaySetInstanceUID = displaySetInstanceUIDs[0]
+ UIModalService.show({
+ content: DicomTagBrowser,
+ contentProps: {
+ displaySets,
+ displaySetInstanceUID,
+ onClose: UIModalService.hide,
+ },
+ title: 'DICOM Tag Browser'
+ });
+ }
};
const definitions = {
@@ -46,6 +73,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
+ openDICOMTagViewer: {
+ commandFn: actions.openDICOMTagViewer,
+ }
};
return {
diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js
index 12c8f2e1f..8daceca1a 100644
--- a/modes/longitudinal/src/toolbarButtons.js
+++ b/modes/longitudinal/src/toolbarButtons.js
@@ -377,6 +377,19 @@ const toolbarButtons = [
],
'Reset'
),
+ _createActionButton(
+ 'TagBrowser',
+ 'list-bullets',
+ 'Dicom Tag Browser',
+ [
+ {
+ commandName: 'openDICOMTagViewer',
+ commandOptions: {},
+ context: 'DEFAULT',
+ },
+ ],
+ 'Dicom Tag Browser'
+ ),
_createActionButton(
'rotate-right',
'tool-rotate-right',
diff --git a/platform/viewer/pluginConfig.json b/platform/viewer/pluginConfig.json
index 13e38f962..f69de2770 100644
--- a/platform/viewer/pluginConfig.json
+++ b/platform/viewer/pluginConfig.json
@@ -45,4 +45,4 @@
],
"modesFactory": [],
"umd": []
-}
+}
\ No newline at end of file