feat: DicomTagBrowser tool (#2975)
* init bring v2 tag browser to v3, some small bug fixes * moved tag browser to default extension, altered style to be more OHIF-v3-like Co-authored-by: Michael Andersen <Michael.Andersen@rmp.uhn.ca>
This commit is contained in:
parent
eddd510a35
commit
ccf010c0fe
53
extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
Normal file
53
extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
387
extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
Normal file
387
extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
Normal file
@ -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 = (
|
||||||
|
<Typography variant="subtitle" className="mr-5 text-right h-full">
|
||||||
|
Instance Number
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
instanceSelectList = (
|
||||||
|
<div className="dicom-tag-browser-instance-range ml-auto">
|
||||||
|
<Range
|
||||||
|
showValue
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={instanceList.length}
|
||||||
|
value={activeInstance}
|
||||||
|
valueRenderer={value => <p>{value}</p>}
|
||||||
|
onChange={({ target }) => {
|
||||||
|
const instanceIndex = parseInt(target.value);
|
||||||
|
setActiveInstance(instanceIndex);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
instanceSelectList = null;
|
||||||
|
}
|
||||||
|
setTags(getSortedTags(metadata));
|
||||||
|
setMeta(metadata);
|
||||||
|
setInstanceList(instanceList);
|
||||||
|
setDisplaySetList(newDisplaySetList);
|
||||||
|
setIsImageStack(isImageStack);
|
||||||
|
}, [activeDisplaySetInstanceUID, activeInstance, displaySets]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dicom-tag-browser-content">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<Typography variant="subtitle" className="mr-5 text-right h-full">
|
||||||
|
Series
|
||||||
|
</Typography>
|
||||||
|
{instanceSelectTitle}</div>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
|
||||||
|
<div className="w-72">
|
||||||
|
<Select
|
||||||
|
isClearable={false}
|
||||||
|
onChange={onSeriesSelect}
|
||||||
|
options={displaySetList}
|
||||||
|
value={selectedDisplaySetValue}
|
||||||
|
/></div>
|
||||||
|
{instanceSelectList}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dicom-tag-browser-table-wrapper">
|
||||||
|
<DicomTagTable rows={getFormattedRowsFromTags(tags, meta)}></DicomTagTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
56
extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
Normal file
56
extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ColumnHeaders() {
|
||||||
|
return (<div className="border-b-4 border-black"> <div className="pt-3 pb-3 bg-primary-dark ">
|
||||||
|
<div className="container m-auto relative flex flex-col"><div className="flex flex-row w-full">
|
||||||
|
<div className="px-3 w-5/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Tag</span></label></div> <div className="px-3 w-4/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Value Representation</span></label></div> <div className="px-3 w-6/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Keyword</span></label></div> <div className="px-3 w-5/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Value</span></label></div></div></div>
|
||||||
|
|
||||||
|
|
||||||
|
</div></div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DicomTagTable({ rows }) {
|
||||||
|
|
||||||
|
return (<div>{ColumnHeaders()}
|
||||||
|
<div className="container m-auto relative">
|
||||||
|
<table className="w-full text-white">
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, index) => {
|
||||||
|
const className = row.className ? row.className : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="hover:bg-secondary-main transition duration-300 bg-primary-dark" key={`DICOMTagRow-${index}`}>
|
||||||
|
<td style={{ maxWidth: "0px" }} className="px-4 py-2 text-base break-all border-b border-secondary-light w-5/24">
|
||||||
|
<div className="flex"><div className="inline-flex max-w-full">{row[0]}</div></div>
|
||||||
|
</td>
|
||||||
|
<td style={{ maxWidth: "0px" }} className="px-4 py-2 text-base break-all border-b border-secondary-light w-4/24">
|
||||||
|
<div className="flex"><div className="inline-flex max-w-full">{row[1]}</div></div>
|
||||||
|
</td>
|
||||||
|
<td style={{ maxWidth: "0px" }} className="px-4 py-2 text-base break-all border-b border-secondary-light w-6/24">
|
||||||
|
<div className="flex"><div className="inline-flex max-w-full">{row[2]}</div></div>
|
||||||
|
</td>
|
||||||
|
<td style={{ maxWidth: "0px" }} className="px-4 py-2 text-base break-all border-b border-secondary-light w-14/24">
|
||||||
|
<div className="flex"><div className="inline-flex max-w-full">{row[3]}</div></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table></div></div >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DicomTagTable;
|
||||||
65
extensions/default/src/DicomTagBrowser/Range.css
Normal file
65
extensions/default/src/DicomTagBrowser/Range.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
57
extensions/default/src/DicomTagBrowser/Range.tsx
Normal file
57
extensions/default/src/DicomTagBrowser/Range.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
ref={props.ref}
|
||||||
|
type="range"
|
||||||
|
value={props.value}
|
||||||
|
min={props.min}
|
||||||
|
max={props.max}
|
||||||
|
step={props.step || 1}
|
||||||
|
onChange={handleChange}
|
||||||
|
id={props.id}
|
||||||
|
className="range"
|
||||||
|
/>
|
||||||
|
{props.showPercentage && <span>{`${value}%`}</span>}
|
||||||
|
{props.showValue && (
|
||||||
|
<span>
|
||||||
|
{props.valueRenderer
|
||||||
|
? props.valueRenderer(props.value)
|
||||||
|
: props.value}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
@ -1,8 +1,13 @@
|
|||||||
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||||
const {
|
const {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
|
ViewportGridService,
|
||||||
|
DisplaySetService
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -23,6 +28,28 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
previousStage: () => {
|
previousStage: () => {
|
||||||
HangingProtocolService.previousProtocolStage();
|
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 = {
|
const definitions = {
|
||||||
@ -46,6 +73,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
openDICOMTagViewer: {
|
||||||
|
commandFn: actions.openDICOMTagViewer,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -377,6 +377,19 @@ const toolbarButtons = [
|
|||||||
],
|
],
|
||||||
'Reset'
|
'Reset'
|
||||||
),
|
),
|
||||||
|
_createActionButton(
|
||||||
|
'TagBrowser',
|
||||||
|
'list-bullets',
|
||||||
|
'Dicom Tag Browser',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
commandName: 'openDICOMTagViewer',
|
||||||
|
commandOptions: {},
|
||||||
|
context: 'DEFAULT',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'Dicom Tag Browser'
|
||||||
|
),
|
||||||
_createActionButton(
|
_createActionButton(
|
||||||
'rotate-right',
|
'rotate-right',
|
||||||
'tool-rotate-right',
|
'tool-rotate-right',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user