From c923d523826cda7a3a6470c9a3ebf25fc4b77886 Mon Sep 17 00:00:00 2001 From: Ross Date: Thu, 15 Oct 2020 22:54:10 +0100 Subject: [PATCH] Start --- anatomy/__init__.py | 0 anatomy/admin.py | 56 + anatomy/apps.py | 5 + anatomy/forms.py | 16 + anatomy/models.py | 242 + anatomy/static/css/anatomy.css | 96 + anatomy/static/js/anatomy.js | 126 + anatomy/static/js/dwv/appgui.js | 174 + anatomy/static/js/dwv/dwv.js | 26806 ++++++++++++++++ anatomy/static/js/dwv/dwv.min.js | 3 + anatomy/static/js/dwv/i18next.min.js | 2 + anatomy/templates/anatomy/answer.html | 9 + .../templates/anatomy/answer_question.html | 13 + anatomy/templates/anatomy/base.html | 32 + anatomy/templates/anatomy/exam.html | 79 + anatomy/templates/anatomy/exam_list.html | 9 + anatomy/templates/anatomy/exam_overview.html | 9 + anatomy/templates/anatomy/exam_scores.html | 40 + anatomy/templates/anatomy/index.html | 11 + anatomy/templates/anatomy/mark.html | 47 + anatomy/templates/anatomy/mark_overview.html | 20 + .../templates/anatomy/question_detail.html | 14 + anatomy/templates/anatomy/question_list.html | 20 + anatomy/templates/registration/login.html | 12 + anatomy/tests.py | 3 + anatomy/urls.py | 20 + anatomy/views.py | 530 + manage.py | 22 + rad/__init__.py | 0 rad/settings.py | 170 + rad/urls.py | 45 + rad/wsgi.py | 16 + requirements.txt | 13 + templates/base.html | 28 + templates/index.html | 11 + 35 files changed, 28699 insertions(+) create mode 100644 anatomy/__init__.py create mode 100644 anatomy/admin.py create mode 100644 anatomy/apps.py create mode 100644 anatomy/forms.py create mode 100644 anatomy/models.py create mode 100644 anatomy/static/css/anatomy.css create mode 100644 anatomy/static/js/anatomy.js create mode 100644 anatomy/static/js/dwv/appgui.js create mode 100644 anatomy/static/js/dwv/dwv.js create mode 100644 anatomy/static/js/dwv/dwv.min.js create mode 100644 anatomy/static/js/dwv/i18next.min.js create mode 100644 anatomy/templates/anatomy/answer.html create mode 100644 anatomy/templates/anatomy/answer_question.html create mode 100644 anatomy/templates/anatomy/base.html create mode 100644 anatomy/templates/anatomy/exam.html create mode 100644 anatomy/templates/anatomy/exam_list.html create mode 100644 anatomy/templates/anatomy/exam_overview.html create mode 100644 anatomy/templates/anatomy/exam_scores.html create mode 100644 anatomy/templates/anatomy/index.html create mode 100644 anatomy/templates/anatomy/mark.html create mode 100644 anatomy/templates/anatomy/mark_overview.html create mode 100644 anatomy/templates/anatomy/question_detail.html create mode 100644 anatomy/templates/anatomy/question_list.html create mode 100644 anatomy/templates/registration/login.html create mode 100644 anatomy/tests.py create mode 100644 anatomy/urls.py create mode 100644 anatomy/views.py create mode 100755 manage.py create mode 100644 rad/__init__.py create mode 100644 rad/settings.py create mode 100644 rad/urls.py create mode 100644 rad/wsgi.py create mode 100644 requirements.txt create mode 100644 templates/base.html create mode 100644 templates/index.html diff --git a/anatomy/__init__.py b/anatomy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/anatomy/admin.py b/anatomy/admin.py new file mode 100644 index 00000000..ec08fc57 --- /dev/null +++ b/anatomy/admin.py @@ -0,0 +1,56 @@ +from django.contrib import admin +from .models import ( + AnatomyQuestion, + Examination, + QuestionType, + Answers, + HalfMarkAnswers, + IncorrectAnswers, + UserAnswer, + Exam, + Modality, + BodyPart, + Structure, +) + +from reversion.admin import VersionAdmin + +# Register your models here. +class AnatomyAnswersInline(admin.TabularInline): + model = Answers + extra = 1 + + +class HalfMarkAnatomyAnswersInline(admin.TabularInline): + model = HalfMarkAnswers + extra = 1 + + +class IncorrectAnatomyAnswersInline(admin.TabularInline): + model = IncorrectAnswers + extra = 1 + + +class ExamInline(admin.TabularInline): + model = AnatomyQuestion.exams.through + extra = 1 + + +class AnatomyAdmin(VersionAdmin): + inlines = [ + AnatomyAnswersInline, + HalfMarkAnatomyAnswersInline, + IncorrectAnatomyAnswersInline, + ExamInline, + ] + + +admin.site.register(AnatomyQuestion, AnatomyAdmin) +admin.site.register(Examination) +admin.site.register(QuestionType) +admin.site.register(UserAnswer) +admin.site.register(Exam) +admin.site.register(Modality) +admin.site.register(BodyPart) +admin.site.register(Structure) + diff --git a/anatomy/apps.py b/anatomy/apps.py new file mode 100644 index 00000000..574518e0 --- /dev/null +++ b/anatomy/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AnatomyConfig(AppConfig): + name = 'anatomy' diff --git a/anatomy/forms.py b/anatomy/forms.py new file mode 100644 index 00000000..3c0a03e2 --- /dev/null +++ b/anatomy/forms.py @@ -0,0 +1,16 @@ +from django import forms + +from .models import UserAnswer, AnatomyQuestion + + +class AnatomyAnswerForm(forms.ModelForm): + class Meta: + model = UserAnswer + fields = ("answer",) + + +class MarkAnatomyQuestionForm(forms.Form): + # correct = forms.CharField(required=False) + # half_correct = forms.CharField(required=False) + # incorrect = forms.CharField(required=False) + marked_answers = forms.CharField(required=False) diff --git a/anatomy/models.py b/anatomy/models.py new file mode 100644 index 00000000..34cc81df --- /dev/null +++ b/anatomy/models.py @@ -0,0 +1,242 @@ +from django.db import models +from django.utils import timezone + +from django.core.files.storage import FileSystemStorage +from django.conf import settings + +image_storage = FileSystemStorage( + # Physical file location ROOT + location=u"{0}/anatomy/".format(settings.MEDIA_ROOT), + # Url for file + base_url=u"{0}anatomy/".format(settings.MEDIA_URL), +) + + +def image_directory_path(instance, filename): + # file will be uploaded to MEDIA_ROOT/anatomy/picture/ + return u"picture/{0}".format(filename) + + +class Examination(models.Model): + examination = models.CharField(max_length=200) + + def __str__(self): + return self.examination + + +class BodyPart(models.Model): + bodypart = models.CharField(max_length=200) + + def __str__(self): + return self.bodypart + + +class Structure(models.Model): + bodypart = models.CharField(max_length=200) + + def __str__(self): + return self.bodypart + + +class Modality(models.Model): + modality = models.CharField(max_length=200) + + def __str__(self): + return self.modality + + +class QuestionType(models.Model): + text = models.CharField(max_length=400) + + def __str__(self): + return self.text + + +class AnatomyQuestion(models.Model): + # author = models.ForeignKey('auth.User', on_delete=models.CASCADE) + # image = models.ImageField() + # feedback = models.TextField(null=True) + question_type = models.ForeignKey(QuestionType, + on_delete=models.SET_NULL, + null=True) + + image = models.ImageField(upload_to=image_directory_path, + storage=image_storage) + + description = models.CharField( + max_length=400, + help_text= + "Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title." + ) + + examination = models.ForeignKey(Examination, + on_delete=models.SET_NULL, + null=True, blank=True) + modality = models.ForeignKey(Modality, + on_delete=models.SET_NULL, + null=True) + body_part = models.ForeignKey(BodyPart, + on_delete=models.SET_NULL, + null=True, + blank=True) + structure = models.ForeignKey(Structure, + on_delete=models.SET_NULL, + null=True, + blank=True) + created_date = models.DateTimeField(default=timezone.now) + + def __str__(self): + # Get first answer + a = self.answers.all().first() + + # Get associated exams + e = self.exams.all().values_list("name", flat=True) + exams = ", ".join(e) + + if a is None: + return "({})".format(exams) + else: + return "{} ({}) [{}, {}]".format(a.answer, exams, self.modality, + self.structure) + + def GetUnmarkedAnswersString(self): + unmarked_answers = self.GetUnmarkedAnswers() + + if not unmarked_answers: + return "No answers to mark" + return "{} answer unmarked: {}".format(len(unmarked_answers), + ", ".join(unmarked_answers)) + + def GetUnmarkedAnswers(self): + user_answers = set([i.answer for i in self.user_answers.all()]) + unmarked_answers = user_answers - self.GetMarkedAnswers() + + return unmarked_answers + + def GetMarkedAnswers(self): + correct_answers = set([i.answer for i in self.answers.all()]) + half_mark_answers = set( + [i.answer for i in self.half_mark_answers.all()]) + incorrect_answers = set( + [i.answer for i in self.incorrect_answers.all()]) + + marked_answers = correct_answers | half_mark_answers | incorrect_answers + return marked_answers + + +class Answers(models.Model): + question = models.ForeignKey(AnatomyQuestion, + related_name="answers", + on_delete=models.CASCADE) + answer = models.CharField(max_length=500) + + def __str__(self): + return self.answer + + def clean(self): + if self.answer: + self.answer.strip() + + +class HalfMarkAnswers(models.Model): + question = models.ForeignKey(AnatomyQuestion, + related_name="half_mark_answers", + on_delete=models.CASCADE) + answer = models.CharField(max_length=500) + + def __str__(self): + return self.answer + + def clean(self): + if self.answer: + self.answer.strip() + + +class IncorrectAnswers(models.Model): + question = models.ForeignKey(AnatomyQuestion, + related_name="incorrect_answers", + on_delete=models.CASCADE) + answer = models.CharField(max_length=500) + + def __str__(self): + return self.answer + + def clean(self): + if self.answer: + self.answer.strip() + + +class Exam(models.Model): + name = models.CharField(max_length=200) + exam_questions = models.ManyToManyField(AnatomyQuestion, + related_name="exams") + + def __str__(self): + return self.name + + +class UserAnswer(models.Model): + question = models.ForeignKey(AnatomyQuestion, + related_name="user_answers", + on_delete=models.CASCADE) + answer = models.CharField(max_length=500, blank=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL) + + cid = models.IntegerField(blank=True, null=True) + + # Each user answer is associated with a particular exam + exam = models.ForeignKey(Exam, + related_name="exam", + on_delete=models.CASCADE, + null=True) + + flagged = models.BooleanField(default=False) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + try: + exam = self.exam + except (Exam.DoesNotExist, KeyError) as e: + exam = "None" + return "{}/{}/{}: {}".format(exam, self.cid, self.question.id, + self.answer) + + def clean(self): + if self.answer: + self.answer.strip() + + +class CidUserAnswer(models.Model): + """User answers by candidate""" + question = models.ForeignKey(AnatomyQuestion, + related_name="cid_user_answers", + on_delete=models.CASCADE) + answer = models.CharField(max_length=500, blank=True) + + cid = models.IntegerField(blank=True, null=True) + + # Each user answer is associated with a particular exam + exam = models.ForeignKey(Exam, + related_name="cid_user_answers", + on_delete=models.CASCADE, + null=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + def __str__(self): + try: + exam = self.exam + except (Exam.DoesNotExist, KeyError) as e: + exam = "None" + return "{}/{}/{}: {}".format(exam, self.cid, self.question.id, + self.answer) + + def clean(self): + if self.answer: + self.answer.strip() diff --git a/anatomy/static/css/anatomy.css b/anatomy/static/css/anatomy.css new file mode 100644 index 00000000..13742202 --- /dev/null +++ b/anatomy/static/css/anatomy.css @@ -0,0 +1,96 @@ +body { + color: gray; + background-color: black; +} + +.answer-list { + font-size: 30px; +} + +.answer-list .correct { + color: green; +} + +.answer-list .half-correct { + color: yellow; +} + +.answer-list .incorrect { + color: red; +} + +.answer-list .not-marked { + color: lightblue; +} + +.key { + font-size: 20; +} + +.hide { + display: none; +} + +.answered { + color: green; +} + +.unanswered { + color: gray; +} + +#question-list { + list-style: none; + float: right; + padding: 1px; + border: 1px solid gray; + /*max-height: 80%;*/ + height: 100%; + overflow-y: scroll; +} + +.flagged { + color: red; +} + +.not-flagged { +} + +button a { + text-decoration: none; + color: green; +} + +#admin-link { + float: right; +} + +.marking-list { + float: right; +} + +.save { + display: inline-block; + padding: 0.35em 1.2em; + border: 0.1em solid #ffffff; + margin: 0 0.3em 0.3em 0; + border-radius: 0.12em; + box-sizing: border-box; + text-decoration: none; + font-family: "Roboto", sans-serif; + font-weight: 300; + color: #ffffff; + text-align: center; + transition: all 0.2s; + background-color: transparent; +} + +.save:hover { + color: #000000; + background-color: #ffffff; +} + +#stats-plot { + width: 50%; + float: right; +} \ No newline at end of file diff --git a/anatomy/static/js/anatomy.js b/anatomy/static/js/anatomy.js new file mode 100644 index 00000000..05e04981 --- /dev/null +++ b/anatomy/static/js/anatomy.js @@ -0,0 +1,126 @@ +var dwvapp = []; + +var marked_answers = { + "correct": [], + "half-correct": [], + "incorrect": [], +} + + +$(document).ready(function () { + $(".answer-list li").each(function (index, element) { + console.log(element); + $(element).click(function (e) { + + var classes = ['correct', 'half-correct', 'incorrect']; + $(element).each(function () { + this.className = classes[($.inArray(this.className, classes) + 1) % classes.length]; + }); + + prepAnswerData(); + + + + }); + }); + prepAnswerData(); + + if($(".post-form").length > 0) { + $(".post-form").get(0).addEventListener("submit", function (e) { + if($(".not-marked").length > 0) { + e.preventDefault(); // before the code + alert("Ensure all answers are marked first"); + } + + }); + } + + if($(".dwv-container").length) { + $(".dwv-container").append($('
Only for HTML5 compatible browsers...
')); + + var app = new dwv.App(); + dwvapp = app; + //DEBUG + var listenerWL = function (event) { + console.log("event: " + event.type); + console.log(event); + $(".infotc").text(event.wc); + }; + app.addEventListener("wl-width-change", listenerWL); + //app.addEventListener("wl-center-change", listener); + app.init({ + + "containerDivId": "dwv", + "fitToWindow": true, + "isMobile": true, + "gui": ["tool"], + "filters": ["Threshold", "Sharpen", "Sobel"], + "tools": ["Scroll", "WindowLevel", "ZoomAndPan"], // or try "ZoomAndPan" + }); + + + + + var range = document.getElementById("sliceRange"); + range.min = 0; + app.addEventListener("load-end", function () { + range.max = app.getImage().getGeometry().getSize().getNumberOfSlices() - 1; + + if(range.max == 0) { + $(range).hide(); + } + }); + app.addEventListener("slice-change", function () { + range.value = app.getViewController().getCurrentPosition().k; + }); + range.oninput = function () { + var pos = app.getViewController().getCurrentPosition(); + pos.k = this.value; + app.getViewController().setCurrentPosition(pos); + } + + try { + app.loadURLs([$(".dwv-container").data("url")]); + } catch(error) { + toastr.error(error.message); + + } + dwv.gui.appendResetHtml(app); + } + +}); + +function prepAnswerData() { + //$("#id_correct").val($("li.correct").map(function() { + // ans = $(this).text(); + // window.marked_answers["correct"].push(ans); + // return ans + //}).get().join('--//--')); + //$("#id_half_correct").val($("li.half-correct").map(function() { + // ans = $(this).text(); + // window.marked_answers["half-correct"].push(ans); + // return ans; + //}).get().join('--//--')); + //$("#id_incorrect").val($("li.incorrect").map(function() { + // ans = $(this).text(); + // window.marked_answers["incorrect"].push(ans); + // return ans; + //}).get().join('--//--')); + + window.marked_answers["correct"] = []; + window.marked_answers["half-correct"] = []; + window.marked_answers["incorrect"] = []; + $("li.correct").map(function () { + ans = $(this).text(); + window.marked_answers["correct"].push(ans); + }) + $("li.half-correct").map(function () { + ans = $(this).text(); + window.marked_answers["half-correct"].push(ans); + }) + $("li.incorrect").map(function () { + ans = $(this).text(); + window.marked_answers["incorrect"].push(ans); + }) + $("#id_marked_answers").val(JSON.stringify(window.marked_answers)); +} diff --git a/anatomy/static/js/dwv/appgui.js b/anatomy/static/js/dwv/appgui.js new file mode 100644 index 00000000..acfd931c --- /dev/null +++ b/anatomy/static/js/dwv/appgui.js @@ -0,0 +1,174 @@ +/** + * Application GUI. + */ + +// Default window level presets. +dwv.tool.defaultpresets = { + "default (127+/-255)": {"center": 127, "width": 255}, + //"default": {"center": 127, "width": 100}, +}; +dwv.tool.defaultpresets.SC = { + "default (127+/-255)": {"center": 127, "width": 255}, +}; +// Default window level presets for CT. +dwv.tool.defaultpresets.CT = { + "mediastinum": {"center": 40, "width": 400}, + "lung": {"center": -500, "width": 1500}, + "bone": {"center": 500, "width": 2000}, +}; + +// decode query +dwv.utils.decodeQuery = dwv.utils.base.decodeQuery; + +// Window +dwv.gui.getWindowSize = function() { +d = dwv.gui.base.getWindowSize(); +//return {"width": d["width"], "height": d["height"]+100} +h = $(window).height() - ($(document).height()-d["height"]) - 100; +return {"width": d["width"], "height": h} +} + +// Progress +dwv.gui.displayProgress = function (/*percent*/) { /*does nothing*/ }; +// get element +dwv.gui.getElement = dwv.gui.base.getElement; +// refresh +dwv.gui.refreshElement = dwv.gui.base.refreshElement; +// Slider +dwv.gui.Slider = null; +// Tags table +dwv.gui.DicomTags = null; + +// Toolbox +dwv.gui.Toolbox = function (app) +{ + this.setup = function (/*list*/) + { + // does nothing + }; + this.display = function (/*bool*/) + { + // does nothing + }; + this.initialise = function (list) + { + // not wonderful: first one should be scroll is more than one slice + if ( list[0] === false ) { + var inputScroll = app.getElement("scroll-button"); + inputScroll.style.display = "none"; + var inputZoom = app.getElement("zoom-button"); + inputZoom.checked = true; + } + }; +}; + +// Window/level +dwv.gui.WindowLevel = function (app) +{ + this.setup = function () + { + var button = document.createElement("button"); + button.className = "wl-button"; + button.value = "WindowLevel"; + button.onclick = app.onChangeTool; + button.appendChild(document.createTextNode("Window/Level")); + + var node = app.getElement("toolbar"); + node.appendChild(button); + }; + this.display = function (bool) + { + var button = app.getElement("wl-button"); + button.disabled = bool; + }; + this.initialise = function () + { + // clear previous + dwv.html.removeNode(app.getElement("presetSelect")); + dwv.html.removeNode(app.getElement("presetLabel")); + + // create preset select + var select = dwv.html.createHtmlSelect("presetSelect", + app.getViewController().getWindowLevelPresetsNames(), "wl.presets", true); + select.className = "presetSelect"; + select.onchange = app.onChangeWindowLevelPreset; + select.title = "Select w/l preset."; + select.setAttribute("data-inline","true"); + var label = document.createElement("label"); + label.className = "presetLabel"; + label.setAttribute("for", "presetSelect"); + label.appendChild(document.createTextNode("Select Preset")); + + var node = app.getElement("toolbar"); + node.appendChild(label); + node.appendChild(select); + }; +}; + +// Zoom +dwv.gui.ZoomAndPan = function (app) +{ + this.setup = function () + { + var button = document.createElement("button"); + button.className = "zoom-button"; + button.value = "ZoomAndPan"; + button.onclick = app.onChangeTool; + button.appendChild(document.createTextNode("ZoomAndPan")); + + var node = app.getElement("toolbar"); + node.appendChild(button); + }; + this.display = function (bool) + { + var button = app.getElement("zoom-button"); + button.disabled = bool; + }; +}; + +// Scroll +dwv.gui.Scroll = function (app) +{ + this.setup = function () + { + var button = document.createElement("button"); + button.className = "scroll-button"; + button.value = "Scroll"; + button.onclick = app.onChangeTool; + button.appendChild(document.createTextNode("Scroll")); + + var node = app.getElement("toolbar"); + node.appendChild(button); + }; + this.display = function (bool) + { + var button = app.getElement("scroll-button"); + button.disabled = bool; + }; +}; + +dwv.gui.Slider = dwv.gui.base.Slider; +// NEEDS UI +dwv.gui.Filter = dwv.gui.base.Filter; + + +// Filter: threshold +dwv.gui.Threshold = dwv.gui.base.Threshold; +// Filter: sharpen +dwv.gui.Sharpen = dwv.gui.base.Sharpen; +// Filter: sobel +dwv.gui.Sobel = dwv.gui.base.Sobel; + + +//Reset +dwv.gui.appendResetHtml = function (app) +{ + var button = document.createElement("button"); + button.className = "reset-button"; + button.value = "reset"; + button.onclick = app.onDisplayReset; + button.appendChild(document.createTextNode("Reset")); + + var node = app.getElement("toolbar"); + node.appendChild(button); +}; diff --git a/anatomy/static/js/dwv/dwv.js b/anatomy/static/js/dwv/dwv.js new file mode 100644 index 00000000..cb2ef018 --- /dev/null +++ b/anatomy/static/js/dwv/dwv.js @@ -0,0 +1,26806 @@ +// Inspired from umdjs +// See https://github.com/umdjs/umd/blob/master/templates/returnExports.js +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + + // magic-wand-js: not importable? + + define([ + 'i18next', + 'i18next-xhr-backend', + 'i18next-browser-languagedetector', + 'jszip', + 'konva' + ], factory); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + + // i18next-xhr-backend: requires XMlHttpRequest + // Konva (requires 'canvas') and MagicWand are gui specific + // -> deactivated for now... + + module.exports = factory( + require('i18next'), + require('i18next-xhr-backend'), + require('i18next-browser-languagedetector'), + require('jszip'), + null, + null + ); + } else { + // Browser globals (root is window) + root.dwv = factory( + root.i18next, + root.i18nextXHRBackend, + root.i18nextBrowserLanguageDetector, + root.JSZip, + root.Konva, + root.MagicWand + ); + } +}(this, function ( + i18next, + i18nextXHRBackend, + i18nextBrowserLanguageDetector, + JSZip, + Konva, + MagicWand) { + + // similar to what browserify does but reversed + //https://www.contentful.com/blog/2017/01/17/the-global-object-in-javascript/ + var window = typeof window !== 'undefined' ? + window : typeof self !== 'undefined' ? + self : typeof global !== 'undefined' ? + global : {}; + +/** @namespace */ +var dwv = dwv || {}; + +/** + * Main application class. + * @constructor + */ +dwv.App = function () +{ + // Local object + var self = this; + + // Image + var image = null; + // Original image + var originalImage = null; + // Image data array + var imageData = null; + // Image data width + var dataWidth = 0; + // Image data height + var dataHeight = 0; + // Is the data mono-slice? + var isMonoSliceData = 0; + + // Default character set + var defaultCharacterSet; + + // Container div id + var containerDivId = null; + // Display window scale + var windowScale = 1; + // Fit display to window flag + var fitToWindow = false; + // main scale + var scale = 1; + // zoom center + var scaleCenter = {"x": 0, "y": 0}; + // translation + var translation = {"x": 0, "y": 0}; + + // View + var view = null; + // View controller + var viewController = null; + + // Info layer controller + var infoController = null; + + // Dicom tags gui + var tagsGui = null; + + // Drawing list gui + var drawListGui = null; + + // Image layer + var imageLayer = null; + + // Draw controller + var drawController = null; + + // Generic style + var style = new dwv.html.Style(); + + // Toolbox controller + var toolboxController = null; + + // Loadbox + var loadbox = null; + // Current loader + var currentLoader = null; + + // UndoStack + var undoStack = null; + + // listeners + var listeners = {}; + + // help resources path + var helpResourcesPath = "./"; + + /** + * Get the image. + * @return {Image} The associated image. + */ + this.getImage = function () { return image; }; + /** + * Set the view. + * @param {Image} img The associated image. + */ + this.setImage = function (img) + { + image = img; + view.setImage(img); + }; + /** + * Restore the original image. + */ + this.restoreOriginalImage = function () + { + image = originalImage; + view.setImage(originalImage); + }; + /** + * Get the image data array. + * @return {Array} The image data array. + */ + this.getImageData = function () { return imageData; }; + /** + * Is the data mono-slice? + * @return {Boolean} True if the data is mono-slice. + */ + this.isMonoSliceData = function () { return isMonoSliceData; }; + + /** + * Get the main scale. + * @return {Number} The main scale. + */ + this.getScale = function () { return scale / windowScale; }; + + /** + * Get the window scale. + * @return {Number} The window scale. + */ + this.getWindowScale = function () { return windowScale; }; + + /** + * Get the scale center. + * @return {Object} The coordinates of the scale center. + */ + this.getScaleCenter = function () { return scaleCenter; }; + + /** + * Get the translation. + * @return {Object} The translation. + */ + this.getTranslation = function () { return translation; }; + + /** + * Get the view controller. + * @return {Object} The controller. + */ + this.getViewController = function () { return viewController; }; + + /** + * Get the draw controller. + * @return {Object} The controller. + */ + this.getDrawController = function () { return drawController; }; + + /** + * Get the image layer. + * @return {Object} The image layer. + */ + this.getImageLayer = function () { return imageLayer; }; + + /** + * Get the draw stage. + * @return {Object} The draw stage. + */ + this.getDrawStage = function () { + return drawController.getDrawStage(); + }; + + /** + * Get the app style. + * @return {Object} The app style. + */ + this.getStyle = function () { return style; }; + + /** + * Get the help resources path. + * @return {String} The path. + */ + this.getHelpResourcesPath = function () { + return helpResourcesPath; + }; + + /** + * Add a command to the undo stack. + * @param {Object} The command to add. + */ + this.addToUndoStack = function (cmd) { + if ( undoStack !== null ) { + undoStack.add(cmd); + } + }; + + /** + * Initialise the HTML for the application. + */ + this.init = function ( config ) { + containerDivId = config.containerDivId; + // tools + if ( config.tools && config.tools.length !== 0 ) { + // setup the tool list + var toolList = {}; + for ( var t = 0; t < config.tools.length; ++t ) { + var toolName = config.tools[t]; + if ( toolName === "Draw" ) { + if ( config.shapes !== 0 ) { + // setup the shape list + var shapeList = {}; + for ( var s = 0; s < config.shapes.length; ++s ) { + var shapeName = config.shapes[s]; + var shapeFactoryClass = shapeName+"Factory"; + if (typeof dwv.tool[shapeFactoryClass] !== "undefined") { + shapeList[shapeName] = dwv.tool[shapeFactoryClass]; + } + else { + console.warn("Could not initialise unknown shape: "+shapeName); + } + } + toolList.Draw = new dwv.tool.Draw(this, shapeList); + toolList.Draw.addEventListener("draw-create", fireEvent); + toolList.Draw.addEventListener("draw-change", fireEvent); + toolList.Draw.addEventListener("draw-move", fireEvent); + toolList.Draw.addEventListener("draw-delete", fireEvent); + } + } + else if ( toolName === "Filter" ) { + if ( config.filters.length !== 0 ) { + // setup the filter list + var filterList = {}; + for ( var f = 0; f < config.filters.length; ++f ) { + var filterName = config.filters[f]; + if (typeof dwv.tool.filter[filterName] !== "undefined") { + filterList[filterName] = new dwv.tool.filter[filterName](this); + } + else { + console.warn("Could not initialise unknown filter: "+filterName); + } + } + toolList.Filter = new dwv.tool.Filter(filterList, this); + toolList.Filter.addEventListener("filter-run", fireEvent); + toolList.Filter.addEventListener("filter-undo", fireEvent); + } + } + else { + // default: find the tool in the dwv.tool namespace + var toolClass = toolName; + if (typeof dwv.tool[toolClass] !== "undefined") { + toolList[toolClass] = new dwv.tool[toolClass](this); + if (typeof toolList[toolClass].addEventListener !== "undefined") { + toolList[toolClass].addEventListener(fireEvent); + } + } + else { + console.warn("Could not initialise unknown tool: "+toolName); + } + } + } + toolboxController = new dwv.ToolboxController(); + toolboxController.create(toolList, this); + } + // gui + if ( config.gui ) { + // tools + if ( config.gui.indexOf("tool") !== -1 && toolboxController) { + toolboxController.setup(); + } + // load + if ( config.gui.indexOf("load") !== -1 ) { + var loaderList = {}; + for ( var l = 0; l < config.loaders.length; ++l ) { + var loaderName = config.loaders[l]; + var loaderClass = loaderName + "Load"; + // default: find the loader in the dwv.gui namespace + if (typeof dwv.gui[loaderClass] !== "undefined") { + loaderList[loaderName] = new dwv.gui[loaderClass](this); + } + else { + console.warn("Could not initialise unknown loader: "+loaderName); + } + } + loadbox = new dwv.gui.Loadbox(this, loaderList); + loadbox.setup(); + var loaderKeys = Object.keys(loaderList); + for ( var lk = 0; lk < loaderKeys.length; ++lk ) { + loaderList[loaderKeys[lk]].setup(); + } + loadbox.displayLoader(loaderKeys[0]); + } + // undo + if ( config.gui.indexOf("undo") !== -1 ) { + undoStack = new dwv.tool.UndoStack(this); + undoStack.setup(); + } + // DICOM Tags + if ( config.gui.indexOf("tags") !== -1 ) { + tagsGui = new dwv.gui.DicomTags(this); + } + // Draw list + if ( config.gui.indexOf("drawList") !== -1 ) { + drawListGui = new dwv.gui.DrawList(this); + // update list on draw events + this.addEventListener("draw-create", drawListGui.update); + this.addEventListener("draw-change", drawListGui.update); + this.addEventListener("draw-delete", drawListGui.update); + } + // version number + if ( config.gui.indexOf("version") !== -1 ) { + dwv.gui.appendVersionHtml(dwv.getVersion()); + } + // help + if ( config.gui.indexOf("help") !== -1 ) { + var isMobile = true; + if ( config.isMobile !== "undefined" ) { + isMobile = config.isMobile; + } + // help resources path + if ( typeof config.helpResourcesPath !== "undefined" ) { + helpResourcesPath = config.helpResourcesPath; + } + dwv.gui.appendHelpHtml( toolboxController.getToolList(), isMobile, this ); + } + } + + // listen to drag&drop + var box = this.getElement("dropBox"); + if ( box ) { + box.addEventListener("dragover", onDragOver); + box.addEventListener("dragleave", onDragLeave); + box.addEventListener("drop", onDrop); + // initial size + var size = dwv.gui.getWindowSize(); + var dropBoxSize = 2 * size.height / 3; + box.setAttribute("style","width:"+dropBoxSize+"px;height:"+dropBoxSize+"px"); + } + + // possible load from URL + if ( typeof config.skipLoadUrl === "undefined" ) { + var query = dwv.utils.getUriQuery(window.location.href); + // check query + if ( query && typeof query.input !== "undefined" ) { + dwv.utils.decodeQuery(query, this.onInputURLs); + // optional display state + if ( typeof query.state !== "undefined" ) { + var onLoadEnd = function (/*event*/) { + loadStateUrl(query.state); + }; + this.addEventListener( "load-end", onLoadEnd ); + } + } + } + else{ + console.log("Not loading url from address since skipLoadUrl is defined."); + } + + // align layers when the window is resized + if ( config.fitToWindow ) { + fitToWindow = true; + window.onresize = this.onResize; + } + + // default character set + if ( typeof config.defaultCharacterSet !== "undefined" ) { + defaultCharacterSet = config.defaultCharacterSet; + } + }; + + /** + * Get a HTML element associated to the application. + * @param name The name or id to find. + * @return The found element or null. + */ + this.getElement = function (name) + { + return dwv.gui.getElement(containerDivId, name); + }; + + /** + * Reset the application. + */ + this.reset = function () + { + // clear tools + if ( toolboxController ) { + toolboxController.reset(); + } + // clear draw + if ( drawController ) { + drawController.reset(); + } + // clear objects + image = null; + view = null; + isMonoSliceData = false; + // reset undo/redo + if ( undoStack ) { + undoStack = new dwv.tool.UndoStack(this); + undoStack.initialise(); + } + }; + + /** + * Reset the layout of the application. + */ + this.resetLayout = function () { + var previousScale = scale; + var previousSC = scaleCenter; + var previousTrans = translation; + // reset values + scale = windowScale; + scaleCenter = {"x": 0, "y": 0}; + translation = {"x": 0, "y": 0}; + // apply new values + if ( imageLayer ) { + imageLayer.resetLayout(windowScale); + imageLayer.draw(); + } + if ( drawController ) { + drawController.resetStage(windowScale); + } + // fire events + if (previousScale != scale) { + fireEvent({"type": "zoom-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y }); + } + if ( (previousSC.x !== scaleCenter.x || previousSC.y !== scaleCenter.y) || + (previousTrans.x !== translation.x || previousTrans.y !== translation.y)) { + fireEvent({"type": "offset-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y }); + } + }; + + /** + * Add an event listener on the app. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ + this.addEventListener = function (type, listener) + { + if ( typeof listeners[type] === "undefined" ) { + listeners[type] = []; + } + listeners[type].push(listener); + }; + + /** + * Remove an event listener from the app. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ + this.removeEventListener = function (type, listener) + { + if( typeof listeners[type] === "undefined" ) { + return; + } + for ( var i = 0; i < listeners[type].length; ++i ) + { + if ( listeners[type][i] === listener ) { + listeners[type].splice(i,1); + } + } + }; + + /** + * Load a list of files. Can be image files or a state file. + * @param {Array} files The list of files to load. + */ + this.loadFiles = function (files) + { + // has been checked for emptiness. + var ext = files[0].name.split('.').pop().toLowerCase(); + if ( ext === "json" ) { + loadStateFile(files[0]); + } + else { + loadImageFiles(files); + } + }; + + /** + * Load a list of image files. + * @private + * @param {Array} files The list of image files to load. + */ + function loadImageFiles(files) + { + // create IO + var fileIO = new dwv.io.FilesLoader(); + // load data + loadImageData(files, fileIO); + } + + /** + * Load a State file. + * @private + * @param {String} file The state file to load. + */ + function loadStateFile(file) + { + // create IO + var fileIO = new dwv.io.FilesLoader(); + // load data + loadStateData([file], fileIO); + } + + /** + * Load a list of URLs. Can be image files or a state file. + * @param {Array} urls The list of urls to load. + * @param {Array} requestHeaders An array of {name, value} to use as request headers. + */ + this.loadURLs = function (urls, requestHeaders) + { + // has been checked for emptiness. + var ext = urls[0].split('.').pop().toLowerCase(); + if ( ext === "json" ) { + loadStateUrl(urls[0], requestHeaders); + } + else { + loadImageUrls(urls, requestHeaders); + } + }; + + /** + * Abort the current load. + */ + this.abortLoad = function () + { + if ( currentLoader ) { + currentLoader.abort(); + currentLoader = null; + } + }; + + /** + * Load a list of ArrayBuffers. + * @param {Array} data The list of ArrayBuffers to load + * in the form of [{name: "", filename: "", data: data}]. + */ + this.loadImageObject = function (data) + { + // create IO + var memoryIO = new dwv.io.MemoryLoader(); + // create options + var options = {}; + // load data + loadImageData(data, memoryIO, options); + }; + + /** + * Load a list of image URLs. + * @private + * @param {Array} urls The list of urls to load. + * @param {Array} requestHeaders An array of {name, value} to use as request headers. + */ + function loadImageUrls(urls, requestHeaders) + { + // create IO + var urlIO = new dwv.io.UrlsLoader(); + // create options + var options = {'requestHeaders': requestHeaders}; + // load data + loadImageData(urls, urlIO, options); + } + + /** + * Load a State url. + * @private + * @param {String} url The state url to load. + * @param {Array} requestHeaders An array of {name, value} to use as request headers. + */ + function loadStateUrl(url, requestHeaders) + { + // create IO + var urlIO = new dwv.io.UrlsLoader(); + // create options + var options = {'requestHeaders': requestHeaders}; + // load data + loadStateData([url], urlIO, options); + } + + /** + * Load a list of image data. + * @private + * @param {Array} data Array of data to load. + * @param {Object} loader The data loader. + * @param {Object} options Options passed to the final loader. + */ + function loadImageData(data, loader, options) + { + // store loader + currentLoader = loader; + + // allow to cancel + var previousOnKeyDown = window.onkeydown; + window.onkeydown = function (event) { + if (event.ctrlKey && event.keyCode === 88 ) // crtl-x + { + console.log("crtl-x pressed!"); + self.abortLoad(); + } + }; + + // clear variables + self.reset(); + // first data name + var firstName = ""; + if (typeof data[0].name !== "undefined") { + firstName = data[0].name; + } else { + firstName = data[0]; + } + // flag used by scroll to decide wether to activate or not + // TODO: supposing multi-slice for zip files, could not be... + isMonoSliceData = (data.length === 1 && + firstName.split('.').pop().toLowerCase() !== "zip"); + // set IO + loader.setDefaultCharacterSet(defaultCharacterSet); + loader.onload = function (data) { + if ( image ) { + view.append( data.view ); + if ( drawController ) { + //drawController.appendDrawLayer(image.getNumberOfFrames()); + } + } + postLoadInit(data); + }; + loader.onerror = function (error) { handleError(error); }; + loader.onabort = function (error) { handleAbort(error); }; + loader.onloadend = function (/*event*/) { + window.onkeydown = previousOnKeyDown; + if ( drawController ) { + drawController.activateDrawLayer(viewController); + } + fireEvent({type: "load-progress", lengthComputable: true, + loaded: 100, total: 100}); + fireEvent({ 'type': 'load-end' }); + // reset member + currentLoader = null; + }; + loader.onprogress = onLoadProgress; + // main load (asynchronous) + fireEvent({ 'type': 'load-start' }); + loader.load(data, options); + } + + /** + * Load a State data. + * @private + * @param {Array} data Array of data to load. + * @param {Object} loader The data loader. + * @param {Object} options Options passed to the final loader. + */ + function loadStateData(data, loader, options) + { + // set IO + loader.onload = function (data) { + // load state + var state = new dwv.State(); + state.apply( self, state.fromJSON(data) ); + }; + loader.onerror = function (error) { handleError(error); }; + // main load (asynchronous) + loader.load(data, options); + } + + /** + * Fit the display to the given size. To be called once the image is loaded. + */ + this.fitToSize = function (size) + { + // previous width + var oldWidth = parseInt(windowScale*dataWidth, 10); + // find new best fit + windowScale = Math.min( (size.width / dataWidth), (size.height / dataHeight) ); + // new sizes + var newWidth = parseInt(windowScale*dataWidth, 10); + var newHeight = parseInt(windowScale*dataHeight, 10); + // ratio previous/new to add to zoom + var mul = newWidth / oldWidth; + scale *= mul; + + // update style + style.setScale(windowScale); + + // resize container + var container = this.getElement("layerContainer"); + container.setAttribute("style","width:"+newWidth+"px;height:"+newHeight+"px"); + // resize image layer + if ( imageLayer ) { + imageLayer.setWidth(newWidth); + imageLayer.setHeight(newHeight); + imageLayer.zoom(scale, scale, 0, 0); + imageLayer.draw(); + } + // resize draw stage + if ( drawController ) { + drawController.resizeStage(newWidth, newHeight, scale); + } + }; + + /** + * Toggle the display of the information layer. + */ + this.toggleInfoLayerDisplay = function () + { + // toggle html + var infoLayer = self.getElement("infoLayer"); + dwv.html.toggleDisplay(infoLayer); + // toggle listeners + infoController.toggleListeners(self, view); + }; + + /** + * Init the Window/Level display + */ + this.initWLDisplay = function () + { + // set window/level to first preset + viewController.setWindowLevelPresetById(0); + // default position + viewController.setCurrentPosition2D(0,0); + // default frame + viewController.setCurrentFrame(0); + }; + + /** + * Add canvas mouse and touch listeners. + * @param {Object} canvas The canvas to listen to. + */ + this.addToolCanvasListeners = function (layer) + { + toolboxController.addCanvasListeners(layer); + }; + + /** + * Remove layer mouse and touch listeners. + * @param {Object} canvas The canvas to stop listening to. + */ + this.removeToolCanvasListeners = function (layer) + { + toolboxController.removeCanvasListeners(layer); + }; + + /** + * Render the current image. + */ + this.render = function () + { + generateAndDrawImage(); + }; + + /** + * Zoom to the layers. + * @param {Number} zoom The zoom to apply. + * @param {Number} cx The zoom center X coordinate. + * @param {Number} cy The zoom center Y coordinate. + */ + this.zoom = function (zoom, cx, cy) { + scale = zoom * windowScale; + if ( scale <= 0.1 ) { + scale = 0.1; + } + scaleCenter = {"x": cx, "y": cy}; + zoomLayers(); + }; + + /** + * Add a step to the layers zoom. + * @param {Number} step The zoom step increment. A good step is of 0.1. + * @param {Number} cx The zoom center X coordinate. + * @param {Number} cy The zoom center Y coordinate. + */ + this.stepZoom = function (step, cx, cy) { + scale += step; + if ( scale <= 0.1 ) { + scale = 0.1; + } + scaleCenter = {"x": cx, "y": cy}; + zoomLayers(); + }; + + /** + * Apply a translation to the layers. + * @param {Number} tx The translation along X. + * @param {Number} ty The translation along Y. + */ + this.translate = function (tx, ty) + { + translation = {"x": tx, "y": ty}; + translateLayers(); + }; + + /** + * Add a translation to the layers. + * @param {Number} tx The step translation along X. + * @param {Number} ty The step translation along Y. + */ + this.stepTranslate = function (tx, ty) + { + var txx = translation.x + tx / scale; + var tyy = translation.y + ty / scale; + translation = {"x": txx, "y": tyy}; + translateLayers(); + }; + + /** + * Get the list of drawing display details. + * @return {Object} The list of draw details including id, slice, frame... + */ + this.getDrawDisplayDetails = function () + { + return drawController.getDrawDisplayDetails(); + }; + + /** + * Get a list of drawing store details. + * @return {Object} A list of draw details including id, text, quant... + */ + this.getDrawStoreDetails = function () + { + return drawController.getDrawStoreDetails(); + }; + /** + * Set the drawings on the current stage. + * @param {Array} drawings An array of drawings. + * @param {Array} drawingsDetails An array of drawings details. + */ + this.setDrawings = function (drawings, drawingsDetails) + { + drawController.setDrawings(drawings, drawingsDetails, fireEvent, this.addToUndoStack); + drawController.activateDrawLayer(viewController); + }; + /** + * Update a drawing from its details. + * @param {Object} drawDetails Details of the drawing to update. + */ + this.updateDraw = function (drawDetails) + { + drawController.updateDraw(drawDetails); + }; + /** + * Delete all Draws from all layers. + */ + this.deleteDraws = function () { + drawController.deleteDraws(fireEvent, this.addToUndoStack); + }; + /** + * Check the visibility of a given group. + * @param {Object} drawDetails Details of the drawing to check. + */ + this.isGroupVisible = function (drawDetails) + { + return drawController.isGroupVisible(drawDetails); + }; + /** + * Toggle group visibility. + * @param {Object} drawDetails Details of the drawing to update. + */ + this.toogleGroupVisibility = function (drawDetails) + { + drawController.toogleGroupVisibility(drawDetails); + }; + + // Handler Methods ----------------------------------------------------------- + + /** + * Handle window/level change. + * @param {Object} event The event fired when changing the window/level. + */ + this.onWLChange = function (event) + { + // generate and draw if no skip flag + if (typeof event.skipGenerate === "undefined" || + event.skipGenerate === false) { + generateAndDrawImage(); + } + }; + + /** + * Handle colour map change. + * @param {Object} event The event fired when changing the colour map. + */ + this.onColourChange = function (/*event*/) + { + generateAndDrawImage(); + }; + + /** + * Handle frame change. + * @param {Object} event The event fired when changing the frame. + */ + this.onFrameChange = function (/*event*/) + { + generateAndDrawImage(); + if ( drawController ) { + drawController.activateDrawLayer(viewController); + } + }; + + /** + * Handle slice change. + * @param {Object} event The event fired when changing the slice. + */ + this.onSliceChange = function (/*event*/) + { + generateAndDrawImage(); + if ( drawController ) { + drawController.activateDrawLayer(viewController); + } + }; + + /** + * Handle key down event. + * - CRTL-Z: undo + * - CRTL-Y: redo + * - CRTL-ARROW_LEFT: next frame + * - CRTL-ARROW_UP: next slice + * - CRTL-ARROW_RIGHT: previous frame + * - CRTL-ARROW_DOWN: previous slice + * Default behavior. Usually used in tools. + * @param {Object} event The key down event. + */ + this.onKeydown = function (event) + { + if (event.ctrlKey) { + if ( event.keyCode === 37 ) // crtl-arrow-left + { + event.preventDefault(); + self.getViewController().decrementFrameNb(); + } + else if ( event.keyCode === 38 ) // crtl-arrow-up + { + event.preventDefault(); + self.getViewController().incrementSliceNb(); + } + else if ( event.keyCode === 39 ) // crtl-arrow-right + { + event.preventDefault(); + self.getViewController().incrementFrameNb(); + } + else if ( event.keyCode === 40 ) // crtl-arrow-down + { + event.preventDefault(); + self.getViewController().decrementSliceNb(); + } + else if ( event.keyCode === 89 ) // crtl-y + { + undoStack.redo(); + } + else if ( event.keyCode === 90 ) // crtl-z + { + undoStack.undo(); + } + } + }; + + /** + * Handle resize. + * Fit the display to the window. To be called once the image is loaded. + * @param {Object} event The change event. + */ + this.onResize = function (/*event*/) + { + self.fitToSize(dwv.gui.getWindowSize()); + }; + + /** + * Handle zoom reset. + * @param {Object} event The change event. + */ + this.onZoomReset = function (/*event*/) + { + self.resetLayout(); + }; + + /** + * Handle loader change. Will activate the loader using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeLoader = function (event) + { + loadbox.displayLoader( event.currentTarget.value ); + }; + + /** + * Reset the load box to its original state. + */ + this.resetLoadbox = function () + { + loadbox.reset(); + }; + + /** + * Handle change url event. + * @param {Object} event The event fired when changing the url field. + */ + this.onChangeURL = function (event) + { + self.loadURLs([event.target.value]); + }; + + /** + * Handle input urls. + * @param {Array} urls The list of input urls. + * @param {Array} requestHeaders An array of {name, value} to use as request headers. + */ + this.onInputURLs = function (urls, requestHeaders) + { + self.loadURLs(urls, requestHeaders); + }; + + /** + * Handle change files event. + * @param {Object} event The event fired when changing the file field. + */ + this.onChangeFiles = function (event) + { + var files = event.target.files; + if ( files.length !== 0 ) { + self.loadFiles(files); + } + }; + + /** + * Handle state save event. + * @param {Object} event The event fired when changing the state save field. + */ + this.onStateSave = function (/*event*/) + { + var state = new dwv.State(); + // add href to link (html5) + var element = self.getElement("download-state"); + element.href = "data:application/json," + state.toJSON(self); + }; + + /** + * Handle colour map change. Will activate the tool using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeColourMap = function (event) + { + viewController.setColourMapFromName( event.currentTarget.value ); + }; + + /** + * Handle window/level preset change. Will activate the preset using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeWindowLevelPreset = function (event) + { + viewController.setWindowLevelPreset( event.currentTarget.value ); + }; + + /** + * Handle tool change. Will activate the tool using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeTool = function (event) + { + toolboxController.setSelectedTool( event.currentTarget.value ); + }; + + /** + * Handle shape change. Will activate the shape using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeShape = function (event) + { + toolboxController.setSelectedShape( event.currentTarget.value ); + }; + + /** + * Handle filter change. Will activate the filter using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeFilter = function (event) + { + toolboxController.setSelectedFilter( event.currentTarget.value ); + }; + + /** + * Handle filter run. + * @param {Object} event The run event. + */ + this.onRunFilter = function (/*event*/) + { + toolboxController.runSelectedFilter(); + }; + + /** + * Handle line colour change. Will activate the colour using + * the value property of the 'event.currentTarget'. + * @param {Object} event The change event. + */ + this.onChangeLineColour = function (event) + { + // called from an HTML select, use its value + toolboxController.setLineColour( event.currentTarget.value ); + }; + + /** + * Handle min/max slider change. + * @param {Object} range The new range of the data. + */ + this.onChangeMinMax = function (range) + { + toolboxController.setRange(range); + }; + + /** + * Handle undo. + * @param {Object} event The associated event. + */ + this.onUndo = function (/*event*/) + { + undoStack.undo(); + }; + + /** + * Handle redo. + * @param {Object} event The associated event. + */ + this.onRedo = function (/*event*/) + { + undoStack.redo(); + }; + + /** + * Handle toggle of info layer. + * @param {Object} event The associated event. + */ + this.onToggleInfoLayer = function (/*event*/) + { + self.toggleInfoLayerDisplay(); + }; + + /** + * Handle display reset. + * @param {Object} event The change event. + */ + this.onDisplayReset = function (/*event*/) + { + self.resetLayout(); + self.initWLDisplay(); + // update preset select + var select = self.getElement("presetSelect"); + if (select) { + select.selectedIndex = 0; + dwv.gui.refreshElement(select); + } + }; + + + // Private Methods ----------------------------------------------------------- + + /** + * Fire an event: call all associated listeners. + * @param {Object} event The event to fire. + */ + function fireEvent (event) + { + if ( typeof listeners[event.type] === "undefined" ) { + return; + } + for ( var i = 0; i < listeners[event.type].length; ++i ) + { + listeners[event.type][i](event); + } + } + + /** + * Generate the image data and draw it. + */ + function generateAndDrawImage() + { + // generate image data from DICOM + view.generateImageData(imageData); + // set the image data of the layer + imageLayer.setImageData(imageData); + // draw the image + imageLayer.draw(); + } + + /** + * Apply the stored zoom to the layers. + */ + function zoomLayers() + { + // image layer + if( imageLayer ) { + imageLayer.zoom(scale, scale, scaleCenter.x, scaleCenter.y); + imageLayer.draw(); + } + // draw layer + if( drawController ) { + drawController.zoomStage(scale, scaleCenter); + } + // fire event + fireEvent({"type": "zoom-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y }); + } + + /** + * Apply the stored translation to the layers. + */ + function translateLayers() + { + // image layer + if( imageLayer ) { + imageLayer.translate(translation.x, translation.y); + imageLayer.draw(); + // draw layer + if( drawController ) { + var ox = - imageLayer.getOrigin().x / scale - translation.x; + var oy = - imageLayer.getOrigin().y / scale - translation.y; + drawController.translateStage(ox, oy); + } + // fire event + fireEvent({"type": "offset-change", "scale": scale, + "cx": imageLayer.getTrans().x, "cy": imageLayer.getTrans().y }); + } + } + + /** + * Handle a drag over. + * @private + * @param {Object} event The event to handle. + */ + function onDragOver(event) + { + // prevent default handling + event.stopPropagation(); + event.preventDefault(); + // update box + var box = self.getElement("dropBox"); + if ( box ) { + box.className = 'dropBox hover'; + } + } + + /** + * Handle a drag leave. + * @private + * @param {Object} event The event to handle. + */ + function onDragLeave(event) + { + // prevent default handling + event.stopPropagation(); + event.preventDefault(); + // update box + var box = self.getElement("dropBox hover"); + if ( box ) { + box.className = 'dropBox'; + } + } + + /** + * Handle a drop event. + * @private + * @param {Object} event The event to handle. + */ + function onDrop(event) + { + // prevent default handling + event.stopPropagation(); + event.preventDefault(); + // load files + self.loadFiles(event.dataTransfer.files); + } + + /** + * Handle an error: display it to the user. + * @private + * @param {Object} error The error to handle. + */ + function handleError(error) + { + // alert window + if ( error.name && error.message) { + alert(error.name+": "+error.message); + } + else { + alert("Error: "+error+"."); + } + // log + if ( error.stack ) { + console.error(error.stack); + } + // stop progress + dwv.gui.displayProgress(100); + } + + /** + * Handle an abort: display it to the user. + * @param {Object} error The error to handle. + * @private + */ + function handleAbort(error) + { + // log + if ( error.message ) { + console.warn(error.message); + } + else { + console.warn("Abort called."); + } + // stop progress + dwv.gui.displayProgress(100); + } + + /** + * Handle a load progress. + * @private + * @param {Object} event The event to handle. + */ + function onLoadProgress(event) + { + fireEvent(event); + if( event.lengthComputable ) + { + var percent = Math.ceil((event.loaded / event.total) * 100); + dwv.gui.displayProgress(percent); + } + } + + /** + * Create the application layers. + * @private + * @param {Number} dataWidth The width of the input data. + * @param {Number} dataHeight The height of the input data. + */ + function createLayers(dataWidth, dataHeight) + { + // image layer + var canImgLay = self.getElement("imageLayer"); + imageLayer = new dwv.html.Layer(canImgLay); + imageLayer.initialise(dataWidth, dataHeight); + imageLayer.fillContext(); + imageLayer.setStyleDisplay(true); + // draw layer + var drawDiv = self.getElement("drawDiv"); + if ( drawDiv ) { + drawController = new dwv.DrawController(drawDiv); + drawController.create(dataWidth, dataHeight); + } + // resize app + if ( fitToWindow ) { + self.fitToSize( dwv.gui.getWindowSize() ); + } + else { + self.fitToSize( { + 'width': self.getElement("layerContainer").offsetWidth, + 'height': self.getElement("layerContainer").offsetHeight } ); + } + self.resetLayout(); + } + + /** + * Post load application initialisation. To be called once the DICOM has been parsed. + * @private + * @param {Object} data The data to display. + */ + function postLoadInit(data) + { + // only initialise the first time + if ( view ) { + return; + } + + // get the view from the loaded data + view = data.view; + viewController = new dwv.ViewController(view); + + // append the DICOM tags table + if ( tagsGui ) { + tagsGui.update(data.info); + } + // store image + originalImage = view.getImage(); + image = originalImage; + + // layout + var size = image.getGeometry().getSize(); + dataWidth = size.getNumberOfColumns(); + dataHeight = size.getNumberOfRows(); + createLayers(dataWidth, dataHeight); + + // get the image data from the image layer + imageData = imageLayer.getContext().createImageData( + dataWidth, dataHeight); + + // image listeners + view.addEventListener("wl-width-change", self.onWLChange); + view.addEventListener("wl-center-change", self.onWLChange); + view.addEventListener("colour-change", self.onColourChange); + view.addEventListener("slice-change", self.onSliceChange); + view.addEventListener("frame-change", self.onFrameChange); + + // connect with local listeners + view.addEventListener("wl-width-change", fireEvent); + view.addEventListener("wl-center-change", fireEvent); + view.addEventListener("colour-change", fireEvent); + view.addEventListener("position-change", fireEvent); + view.addEventListener("slice-change", fireEvent); + view.addEventListener("frame-change", fireEvent); + + // append draw layers (before initialising the toolbox) + if ( drawController ) { + //drawController.appendDrawLayer(image.getNumberOfFrames()); + } + + // initialise the toolbox + if ( toolboxController ) { + toolboxController.initAndDisplay( imageLayer ); + } + + // stop box listening to drag (after first drag) + var box = self.getElement("dropBox"); + if ( box ) { + box.removeEventListener("dragover", onDragOver); + box.removeEventListener("dragleave", onDragLeave); + box.removeEventListener("drop", onDrop); + dwv.html.removeNode(box); + // switch listening to layerContainer + var div = self.getElement("layerContainer"); + div.addEventListener("dragover", onDragOver); + div.addEventListener("dragleave", onDragLeave); + div.addEventListener("drop", onDrop); + } + + // info layer + var infoLayer = self.getElement("infoLayer"); + if ( infoLayer ) { + infoController = new dwv.InfoController(containerDivId); + infoController.create(self); + infoController.toggleListeners(self, view); + } + + // init W/L display + self.initWLDisplay(); + // generate first image + generateAndDrawImage(); + } + +}; + +// namespaces +var dwv = dwv || {}; +dwv.draw = dwv.draw || {}; +// external +var Konva = Konva || {}; + +/** + * Get the draw group id for a given position. + * @return {Number} The group id. + */ +dwv.draw.getDrawPositionGroupId = function (sliceNumber, frameNumber) { + return "slice-"+sliceNumber+"_frame-"+frameNumber; +}; + +/** + * Get the slice and frame position from a group id. + * @param {String} groupId The group id. + * @return {Object} The slice and frame number. + */ +dwv.draw.getPositionFromGroupId = function (groupId) { + var sepIndex = groupId.indexOf("_"); + if (sepIndex === -1) { + console.warn("Badly formed PositionGroupId: "+groupId); + } + return { 'sliceNumber': groupId.substring(6, sepIndex), + 'frameNumber': groupId.substring(sepIndex + 7) }; +}; + +/** + * Is an input node's name 'shape'. + * @param {Object} node A Konva node. + */ +dwv.draw.isNodeNameShape = function ( node ) { + return node.name() === "shape"; +}; + +/** + * Is a node an extra shape associated with a main one. + * @param {Object} node A Konva node. + */ +dwv.draw.isNodeNameShapeExtra = function ( node ) { + return node.name().startsWith("shape-"); +}; + +/** + * Is an input node's name 'label'. + * @param {Object} node A Konva node. + */ +dwv.draw.isNodeNameLabel = function ( node ) { + return node.name() === "label"; +}; + +/** + * Is an input node a position node. + * @param {Object} node A Konva node. + */ +dwv.draw.isPositionNode = function ( node ) { + return node.name() === 'position-group'; +}; + +/** + * Get a lambda to check a node's id. + * @param {String} id The id to check. + * @return A function to check a node's id. + */ +dwv.draw.isNodeWithId = function ( id ) { + return function (node) { + return node.id() === id; + }; +}; + +/** + * Is the input node a node that has the 'stroke' method. + * @param {Object} node A Konva node. +*/ +dwv.draw.canNodeChangeColour = function ( node ) { + return node.name() !== "anchor" && node.name() !== "label"; +}; + +/** + * Debug function to output the layer hierarchy as text. + * @param {Object} layer The Konva layer. + * @param {String} prefix A display prefix (used in recursion). + * @return {String} A text representation of the hierarchy. + */ +dwv.draw.getHierarchyLog = function (layer, prefix) { + if ( typeof prefix === "undefined" ) { + prefix = ""; + } + var kids = layer.getChildren(); + var log = prefix + "|__ " + layer.name() + ": " + layer.id() + "\n"; + for ( var i = 0; i < kids.length; ++i ) { + log += dwv.draw.getHierarchyLog( kids[i], prefix + " "); + } + return log; +}; + +/** + * Draw controller. + * @constructor + * @param {Object} drawDiv The HTML div used to store the drawings. + * @external Konva + */ +dwv.DrawController = function (drawDiv) +{ + // Draw stage + var drawStage = null; + // Draw layer + var drawLayer; + // current position group id + var currentPosGroupId = null; + + /** + * Get the current position group. + * @return {Object} The Konva.Group. + */ + this.getCurrentPosGroup = function () { + // get position groups + var posGroups = drawLayer.getChildren( function (node) { + return node.id() === currentPosGroupId; + }); + // if one group, use it + // if no group, create one + var posGroup = null; + if ( posGroups.length === 1 ) { + posGroup = posGroups[0]; + } else if ( posGroups.length === 0 ) { + posGroup = new Konva.Group(); + posGroup.name("position-group"); + posGroup.id(currentPosGroupId); + posGroup.visible(true); // dont inherit + // add new group to layer + drawLayer.add(posGroup); + } else { + console.warn("Unexpected number of draw position groups."); + } + // return + return posGroup; + }; + + /** + * Create the controller: sets up the draw stage. + * @param {Number} width The width of the stage. + * @param {Number} height The height of the stage. + */ + this.create = function (width, height) { + // create stage + drawStage = new Konva.Stage({ + 'container': drawDiv, + 'width': width, + 'height': height, + 'listening': false + }); + // reset style + // (avoids a not needed vertical scrollbar) + drawStage.getContent().setAttribute("style", ""); + + // create layer + drawLayer = new Konva.Layer({ + 'listening': false, + 'hitGraphEnabled': false, + 'visible': true + }); + drawStage.add(drawLayer); + }; + + /** + * Get the draw layer. + * @return {Object} The draw layer. + */ + this.getDrawLayer = function () { + return drawLayer; + }; + + /** + * Reset: clear the layers array. + */ + this.reset = function () { + drawLayer = null; + }; + + /** + * Get the draw stage. + * @return {Object} The draw layer. + */ + this.getDrawStage = function () { + return drawStage; + }; + + /** + * Activate the current draw layer. + * @param {Object} viewController The associated view controller. + */ + this.activateDrawLayer = function (viewController) + { + // set current position + var currentSlice = viewController.getCurrentPosition().k; + var currentFrame = viewController.getCurrentFrame(); + // get and store the position group id + currentPosGroupId = dwv.draw.getDrawPositionGroupId(currentSlice,currentFrame); + + // get all position groups + var posGroups = drawLayer.getChildren( dwv.draw.isPositionNode ); + // reset or set the visible property + var visible; + for ( var i = 0, leni = posGroups.length; i < leni; ++i ) { + visible = false; + if ( posGroups[i].id() === currentPosGroupId ) { + visible = true; + } + // group members inherit the visible property + posGroups[i].visible(visible); + } + + // show current draw layer + drawLayer.draw(); + }; + + /** + * Reset the stage with a new window scale. + * @param {Number} windowScale The window scale. + */ + this.resetStage = function (windowScale) { + drawStage.offset( {'x': 0, 'y': 0} ); + drawStage.scale( {'x': windowScale, 'y': windowScale} ); + drawStage.draw(); + }; + + /** + * Resize the current stage. + * @param {Number} width the stage width. + * @param {Number} height the stage height. + * @param {Number} scale the stage scale. + */ + this.resizeStage = function (width, height, scale) { + // resize div + drawDiv.setAttribute("style","width:"+width+"px;height:"+height+"px"); + // resize stage + drawStage.setWidth(width); + drawStage.setHeight(height); + drawStage.scale( {'x': scale, 'y': scale} ); + drawStage.draw(); + }; + + /** + * Zoom the stage. + * @param {Number} scale The scale factor. + * @param {Object} scaleCenter The scale center point. + */ + this.zoomStage = function (scale, scaleCenter) { + // zoom + var newScale = {'x': scale, 'y': scale}; + // offset + // TODO different from the imageLayer offset? + var oldScale = drawStage.scale(); + var oldOffset = drawStage.offset(); + var newOffsetX = (scaleCenter.x / oldScale.x) + + oldOffset.x - (scaleCenter.x / newScale.x); + var newOffsetY = (scaleCenter.y / oldScale.y) + + oldOffset.y - (scaleCenter.y / newScale.y); + var newOffset = {'x': newOffsetX, 'y': newOffsetY}; + // store + drawStage.offset( newOffset ); + drawStage.scale( newScale ); + drawStage.draw(); + }; + + /** + * Translate the stage. + * @param {Number} tx The X translation. + * @param {Number} ty The Y translation. + */ + this.translateStage = function (tx, ty) { + drawStage.offset( {'x': tx, 'y': ty} ); + drawStage.draw(); + }; + + /** + * Get a list of drawing display details. + * @return {Object} A list of draw details including id, slice, frame... + */ + this.getDrawDisplayDetails = function () + { + var list = []; + var groups = drawLayer.getChildren(); + for ( var j = 0, lenj = groups.length; j < lenj; ++j ) { + var position = dwv.draw.getPositionFromGroupId(groups[j].id()); + var collec = groups[j].getChildren(); + for ( var i = 0, leni = collec.length; i < leni; ++i ) { + var shape = collec[i].getChildren( dwv.draw.isNodeNameShape )[0]; + var label = collec[i].getChildren( dwv.draw.isNodeNameLabel )[0]; + var text = label.getChildren()[0]; + var type = shape.className; + if (type === "Line") { + var shapeExtrakids = collec[i].getChildren( dwv.draw.isNodeNameShapeExtra ); + if (shape.closed()) { + type = "Roi"; + } else if (shapeExtrakids.length !== 0) { + if ( shapeExtrakids[0].name().indexOf("triangle") !== -1 ) { + type = "Arrow"; + } + else { + type = "Ruler"; + } + } + } + if (type === "Rect") { + type = "Rectangle"; + } + list.push( { + "id": collec[i].id(), + "slice": position.sliceNumber, + "frame": position.frameNumber, + "type": type, + "color": shape.stroke(), + "label": text.textExpr, + "description": text.longText + }); + } + } + return list; + }; + + /** + * Get a list of drawing store details. + * @return {Object} A list of draw details including id, text, quant... + * TODO Unify with getDrawDisplayDetails? + */ + this.getDrawStoreDetails = function () + { + var drawingsDetails = {}; + + // get all position groups + var posGroups = drawLayer.getChildren( dwv.draw.isPositionNode ); + + var posKids; + var group; + for ( var i = 0, leni = posGroups.length; i < leni; ++i ) { + posKids = posGroups[i].getChildren(); + for ( var j = 0, lenj = posKids.length; j < lenj; ++j ) { + group = posKids[j]; + // remove anchors + var anchors = group.find(".anchor"); + for ( var a = 0; a < anchors.length; ++a ) { + anchors[a].remove(); + } + // get text + var texts = group.find(".text"); + if ( texts.length !== 1 ) { + console.warn("There should not be more than one text per shape."); + } + // get details (non konva vars) + drawingsDetails[ group.id() ] = { + "textExpr": encodeURIComponent(texts[0].textExpr), + "longText": encodeURIComponent(texts[0].longText), + "quant": texts[0].quant + }; + } + } + return drawingsDetails; + }; + + /** + * Set the drawings on the current stage. + * @param {Array} drawings An array of drawings. + * @param {Array} drawingsDetails An array of drawings details. + * @param {Object} cmdCallback The DrawCommand callback. + * @param {Object} exeCallback The callback to call once the DrawCommand has been executed. + */ + this.setDrawings = function (drawings, drawingsDetails, cmdCallback, exeCallback) + { + // regular Konva deserialize + var stateLayer = Konva.Node.create(drawings); + + // get all position groups + var statePosGroups = stateLayer.getChildren( dwv.draw.isPositionNode ); + + for ( var i = 0, leni = statePosGroups.length; i < leni; ++i ) { + var statePosGroup = statePosGroups[i]; + + // Get or create position-group if it does not exist and append it to drawLayer + var posGroup = drawLayer.getChildren( dwv.draw.isNodeWithId( statePosGroup.id() ) )[0]; + if( typeof posGroup === "undefined" ) { + posGroup = new Konva.Group({ + 'id': statePosGroup.id(), + 'name': "position-group", + 'visible': false + }); + drawLayer.add(posGroup); + } + + var statePosKids = statePosGroup.getChildren(); + for ( var j = 0, lenj = statePosKids.length; j < lenj; ++j ) { + // shape group + var stateGroup = statePosKids[j]; + // add group to posGroup (switches its parent) + posGroup.add( stateGroup ); + // shape + var shape = stateGroup.getChildren( dwv.draw.isNodeNameShape )[0]; + // create the draw command + var cmd = new dwv.tool.DrawGroupCommand( + stateGroup, shape.className, + drawLayer ); + // draw command callbacks + cmd.onExecute = cmdCallback; + cmd.onUndo = cmdCallback; + // details + if (drawingsDetails) { + var details = drawingsDetails[ stateGroup.id() ]; + var label = stateGroup.getChildren( dwv.draw.isNodeNameLabel )[0]; + var text = label.getText(); + // store details + text.textExpr = details.textExpr; + text.longText = details.longText; + text.quant = details.quant; + // reset text (it was not encoded) + text.setText(dwv.utils.replaceFlags(text.textExpr, text.quant)); + } + // execute + cmd.execute(); + exeCallback(cmd); + } + } + }; + + /** + * Update a drawing from its details. + * @param {Object} drawDetails Details of the drawing to update. + */ + this.updateDraw = function (drawDetails) + { + // get the group + var group = drawLayer.findOne( "#"+drawDetails.id ); + if ( typeof group === "undefined" ) { + console.warn("[updateDraw] Cannot find group with id: "+drawDetails.id); + return; + } + // shape + var shapes = group.getChildren( dwv.draw.isNodeNameShape ); + for (var i = 0; i < shapes.length; ++i ) { + shapes[i].stroke(drawDetails.color); + } + // shape extra + var shapesExtra = group.getChildren( dwv.draw.isNodeNameShapeExtra ); + for (var j = 0; j < shapesExtra.length; ++j ) { + if (typeof shapesExtra[j].stroke() !== "undefined") { + shapesExtra[j].stroke(drawDetails.color); + } + else if (typeof shapesExtra[j].fill() !== "undefined") { + shapesExtra[j].fill(drawDetails.color); + } + } + // label + var label = group.getChildren( dwv.draw.isNodeNameLabel )[0]; + var text = label.getChildren()[0]; + text.fill(drawDetails.color); + text.textExpr = drawDetails.label; + text.longText = drawDetails.description; + text.setText(dwv.utils.replaceFlags(text.textExpr, text.quant)); + + // udpate current layer + drawLayer.draw(); + }; + + /** + * Check the visibility of a given group. + * @param {Object} drawDetails Details of the group to check. + */ + this.isGroupVisible = function (drawDetails) { + // get the group + var group = drawLayer.findOne( "#"+drawDetails.id ); + if ( typeof group === "undefined" ) { + console.warn("[isGroupVisible] Cannot find node with id: "+drawDetails.id); + return false; + } + // get visibility + return group.isVisible(); + }; + + /** + * Toggle the visibility of a given group. + * @param {Object} drawDetails Details of the group to update. + */ + this.toogleGroupVisibility = function (drawDetails) { + // get the group + var group = drawLayer.findOne( "#"+drawDetails.id ); + if ( typeof group === "undefined" ) { + console.warn("[toogleGroupVisibility] Cannot find node with id: "+drawDetails.id); + return false; + } + // toggle visible + group.visible(!group.isVisible()); + + // udpate current layer + drawLayer.draw(); + }; + + /** + * Delete all Draws from the stage. + * @param {Object} cmdCallback The DeleteCommand callback. + * @param {Object} exeCallback The callback to call once the DeleteCommand has been executed. + */ + this.deleteDraws = function (cmdCallback, exeCallback) { + var delcmd; + var groups = drawLayer.getChildren(); + while (groups.length) { + var shape = groups[0].getChildren( dwv.draw.isNodeNameShape )[0]; + delcmd = new dwv.tool.DeleteGroupCommand( groups[0], + dwv.tool.GetShapeDisplayName(shape), drawLayer); + delcmd.onExecute = cmdCallback; + delcmd.onUndo = cmdCallback; + delcmd.execute(); + exeCallback(delcmd); + } + }; + +}; // class dwv.DrawController + +// namespaces +var dwv = dwv || {}; + +/** + * Info controller. + * @constructor + */ +dwv.InfoController = function (containerDivId) +{ + + // Info layer plot gui + var plotInfo = null; + // Info layer colour map gui + var miniColourMap = null; + // Info layer overlay + var overlayInfos = []; + // flag to know if the info layer is listening on the image. + var isInfoLayerListening = false; + + /** + * Create the different info elements. + * TODO Get rid of the app input arg... + */ + this.create = function (app) + { + var infocm = getElement("infocm"); + if (infocm) { + miniColourMap = new dwv.gui.info.MiniColourMap(infocm, app); + miniColourMap.create(); + } + + // create overlay info at each corner + var pos_list = [ + "tl", "tc", "tr", + "cl", "cr", + "bl", "bc", "br" ]; + + var num = 0; + for (var n=0; n 0){ + for (var n=0; n 0){ + for (var n=0; n 0; +}; + +/** + * Get the utfLabel (used by the TextDecoder) from a character set term + * References: + * - DICOM [Value Encoding]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_6.html} + * - DICOM [Specific Character Set]{@link http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2} + * - [TextDecoder#Parameters]{@link https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder#Parameters} + */ +dwv.dicom.getUtfLabel = function (charSetTerm) +{ + var label = "utf-8"; + if (charSetTerm === "ISO_IR 100" ) { + label = "iso-8859-1"; + } + else if (charSetTerm === "ISO_IR 101" ) { + label = "iso-8859-2"; + } + else if (charSetTerm === "ISO_IR 109" ) { + label = "iso-8859-3"; + } + else if (charSetTerm === "ISO_IR 110" ) { + label = "iso-8859-4"; + } + else if (charSetTerm === "ISO_IR 144" ) { + label = "iso-8859-5"; + } + else if (charSetTerm === "ISO_IR 127" ) { + label = "iso-8859-6"; + } + else if (charSetTerm === "ISO_IR 126" ) { + label = "iso-8859-7"; + } + else if (charSetTerm === "ISO_IR 138" ) { + label = "iso-8859-8"; + } + else if (charSetTerm === "ISO_IR 148" ) { + label = "iso-8859-9"; + } + else if (charSetTerm === "ISO_IR 13" ) { + label = "shift-jis"; + } + else if (charSetTerm === "ISO_IR 166" ) { + label = "iso-8859-11"; + } + else if (charSetTerm === "ISO 2022 IR 87" ) { + label = "iso-2022-jp"; + } + else if (charSetTerm === "ISO 2022 IR 149" ) { + // not supported by TextDecoder when it says it should... + //label = "iso-2022-kr"; + } + else if (charSetTerm === "ISO 2022 IR 58") { + // not supported by TextDecoder... + //label = "iso-2022-cn"; + } + else if (charSetTerm === "ISO_IR 192" ) { + label = "utf-8"; + } + else if (charSetTerm === "GB18030" ) { + label = "gb18030"; + } + else if (charSetTerm === "GB2312" ) { + label = "gb2312"; + } + else if (charSetTerm === "GBK" ) { + label = "chinese"; + } + return label; +}; + +/** + * Data reader. + * @constructor + * @param {Array} buffer The input array buffer. + * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian. + */ +dwv.dicom.DataReader = function (buffer, isLittleEndian) +{ + // Set endian flag if not defined. + if ( typeof isLittleEndian === 'undefined' ) { + isLittleEndian = true; + } + + // Default text decoder + var defaultTextDecoder = {}; + defaultTextDecoder.decode = function (buffer) { + var result = ""; + for ( var i = 0, leni = buffer.length; i < leni; ++i ) { + result += String.fromCharCode( buffer[ i ] ); + } + return result; + }; + // Text decoder + var textDecoder = defaultTextDecoder; + if (typeof window.TextDecoder !== "undefined") { + textDecoder = new TextDecoder("iso-8859-1"); + } + + /** + * Set the utfLabel used to construct the TextDecoder. + * @param {String} label The encoding label. + */ + this.setUtfLabel = function (label) { + if (typeof window.TextDecoder !== "undefined") { + textDecoder = new TextDecoder(label); + } + }; + + /** + * Is the Native endianness Little Endian. + * @private + * @type Boolean + */ + var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); + + /** + * Flag to know if the TypedArray data needs flipping. + * @private + * @type Boolean + */ + var needFlip = (isLittleEndian !== isNativeLittleEndian); + + /** + * The main data view. + * @private + * @type DataView + */ + var view = new DataView(buffer); + + /** + * Flip an array's endianness. + * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. + * @param {Object} array The array to flip (modified). + */ + this.flipArrayEndianness = function (array) { + var blen = array.byteLength; + var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); + var bpel = array.BYTES_PER_ELEMENT; + var tmp; + for ( var i = 0; i < blen; i += bpel ) { + for ( var j = i + bpel - 1, k = i; j > k; j--, k++ ) { + tmp = u8[k]; + u8[k] = u8[j]; + u8[j] = tmp; + } + } + }; + + /** + * Read Uint16 (2 bytes) data. + * @param {Number} byteOffset The offset to start reading from. + * @return {Number} The read data. + */ + this.readUint16 = function (byteOffset) { + return view.getUint16(byteOffset, isLittleEndian); + }; + /** + * Read Uint32 (4 bytes) data. + * @param {Number} byteOffset The offset to start reading from. + * @return {Number} The read data. + */ + this.readUint32 = function (byteOffset) { + return view.getUint32(byteOffset, isLittleEndian); + }; + /** + * Read Int32 (4 bytes) data. + * @param {Number} byteOffset The offset to start reading from. + * @return {Number} The read data. + */ + this.readInt32 = function (byteOffset) { + return view.getInt32(byteOffset, isLittleEndian); + }; + /** + * Read Uint8 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readUint8Array = function (byteOffset, size) { + return new Uint8Array(buffer, byteOffset, size); + }; + /** + * Read Int8 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readInt8Array = function (byteOffset, size) { + return new Int8Array(buffer, byteOffset, size); + }; + /** + * Read Uint16 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readUint16Array = function (byteOffset, size) { + var arraySize = size / Uint16Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) + if ( (byteOffset % Uint16Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Uint16Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Uint16Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt16( (byteOffset + + Uint16Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } + } + return data; + }; + /** + * Read Int16 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readInt16Array = function (byteOffset, size) { + var arraySize = size / Int16Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) + if ( (byteOffset % Int16Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Int16Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Int16Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt16( (byteOffset + + Int16Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } + } + return data; + }; + /** + * Read Uint32 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readUint32Array = function (byteOffset, size) { + var arraySize = size / Uint32Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) + if ( (byteOffset % Uint32Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Uint32Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Uint32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getUint32( (byteOffset + + Uint32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } + } + return data; + }; + /** + * Read Int32 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readInt32Array = function (byteOffset, size) { + var arraySize = size / Int32Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) + if ( (byteOffset % Int32Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Int32Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Int32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getInt32( (byteOffset + + Int32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } + } + return data; + }; + /** + * Read Float32 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readFloat32Array = function (byteOffset, size) { + var arraySize = size / Float32Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) + if ( (byteOffset % Float32Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Float32Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Float32Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getFloat32( (byteOffset + + Float32Array.BYTES_PER_ELEMENT * i), + isLittleEndian); + } + } + return data; + }; + /** + * Read Float64 array. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} size The size of the array. + * @return {Array} The read data. + */ + this.readFloat64Array = function (byteOffset, size) { + var arraySize = size / Float64Array.BYTES_PER_ELEMENT; + var data = null; + // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) + if ( (byteOffset % Float64Array.BYTES_PER_ELEMENT) === 0 ) { + data = new Float64Array(buffer, byteOffset, arraySize); + if ( needFlip ) { + this.flipArrayEndianness(data); + } + } + else { + data = new Float64Array(arraySize); + for ( var i = 0; i < arraySize; ++i ) { + data[i] = view.getFloat64( (byteOffset + + Float64Array.BYTES_PER_ELEMENT*i), + isLittleEndian); + } + } + return data; + }; + /** + * Read data as an hexadecimal string. + * @param {Number} byteOffset The offset to start reading from. + * @return {Array} The read data. + */ + this.readHex = function (byteOffset) { + // read and convert to hex string + var str = this.readUint16(byteOffset).toString(16); + // return padded + return "0x0000".substr(0, 6 - str.length) + str.toUpperCase(); + }; + + /** + * Read data as a string. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} nChars The number of characters to read. + * @return {String} The read data. + */ + this.readString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return defaultTextDecoder.decode(data); + }; + + /** + * Read data as a 'special' string, decoding it if the TextDecoder is available. + * @param {Number} byteOffset The offset to start reading from. + * @param {Number} nChars The number of characters to read. + * @return {String} The read data. + */ + this.readSpecialString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return textDecoder.decode(data); + }; + +}; + +/** + * Get the group-element pair from a tag string name. + * @param {String} tagName The tag string name. + * @return {Object} group-element pair. + */ +dwv.dicom.getGroupElementFromName = function (tagName) +{ + var group = null; + var element = null; + var dict = dwv.dicom.dictionary; + var keys0 = Object.keys(dict); + var keys1 = null; + // label for nested loop break + outLabel: + // search through dictionary + for ( var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0 ) { + group = keys0[k0]; + keys1 = Object.keys( dict[group] ); + for ( var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1 ) { + element = keys1[k1]; + if ( dict[group][element][2] === tagName ) { + break outLabel; + } + } + } + return { 'group': group, 'element': element }; +}; + +/** + * Immutable tag. + * @constructor + * @param {String} group The tag group. + * @param {String} element The tag element. + */ +dwv.dicom.Tag = function (group, element) +{ + /** + * Get the tag group. + * @return {String} The tag group. + */ + this.getGroup = function () { return group; }; + /** + * Get the tag element. + * @return {String} The tag element. + */ + this.getElement = function () { return element; }; +}; // Tag class + +/** + * Check for Tag equality. + * @param {Object} rhs The other tag to compare to. + * @return {Boolean} True if both tags are equal. + */ +dwv.dicom.Tag.prototype.equals = function (rhs) { + return rhs !== null && + this.getGroup() === rhs.getGroup() && + this.getElement() === rhs.getElement(); +}; + +/** + * Check for Tag equality. + * @param {Object} rhs The other tag to compare to provided as a simple object. + * @return {Boolean} True if both tags are equal. + */ +dwv.dicom.Tag.prototype.equals2 = function (rhs) { + if (rhs === null || + typeof rhs.group === "undefined" || + typeof rhs.element === "undefined" ) { + return false; + } + return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); +}; + +// Get the FileMetaInformationGroupLength Tag. +dwv.dicom.getFileMetaInformationGroupLengthTag = function () { + return new dwv.dicom.Tag("0x0002", "0x0000"); +}; +// Get the Item Tag. +dwv.dicom.getItemTag = function () { + return new dwv.dicom.Tag("0xFFFE", "0xE000"); +}; +// Get the ItemDelimitationItem Tag. +dwv.dicom.getItemDelimitationItemTag = function () { + return new dwv.dicom.Tag("0xFFFE", "0xE00D"); +}; +// Get the SequenceDelimitationItem Tag. +dwv.dicom.getSequenceDelimitationItemTag = function () { + return new dwv.dicom.Tag("0xFFFE", "0xE0DD"); +}; +// Get the PixelData Tag. +dwv.dicom.getPixelDataTag = function () { + return new dwv.dicom.Tag("0x7FE0", "0x0010"); +}; + +/** + * Get the group-element key used to store DICOM elements. + * @param {Number} group The DICOM group. + * @param {Number} element The DICOM element. + * @return {String} The key. + */ +dwv.dicom.getGroupElementKey = function (group, element) +{ + return 'x' + group.substr(2,6) + element.substr(2,6); +}; + +/** + * Split a group-element key used to store DICOM elements. + * @param {String} key The key in form "x00280102. + * @return {Object} The DICOM group and element. + */ +dwv.dicom.splitGroupElementKey = function (key) +{ + return {'group': key.substr(1,4), 'element': key.substr(5,8) }; +}; + +/** + * Get patient orientation label in the reverse direction. + * @param {String} ori Patient Orientation value. + * @return {String} Reverse Orientation Label. + */ +dwv.dicom.getReverseOrientation = function (ori) +{ + if (!ori) { + return null; + } + // reverse labels + var rlabels = { + "L": "R", + "R": "L", + "A": "P", + "P": "A", + "H": "F", + "F": "H" + }; + + var rori = ""; + for (var n=0; n regular explicit: 8 + X + * | 4 | 2+2 | 4 | X | -> 32bit VL: 12 + X + * + * | Tag | VL | Value | + * | 4 | 4 | X | -> implicit (32bit VL): 8 + X + * + * | Tag | Len | Value | + * | 4 | 4 | X | -> item: 8 + X + */ +dwv.dicom.getDataElementPrefixByteSize = function (vr, isImplicit) { + return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8; +}; + +/** + * DicomParser class. + * @constructor + */ +dwv.dicom.DicomParser = function () +{ + /** + * The list of DICOM elements. + * @type Array + */ + this.dicomElements = {}; + + /** + * Default character set (optional). + * @private + * @type String + */ + var defaultCharacterSet; + /** + * Get the default character set. + * @return {String} The default character set. + */ + this.getDefaultCharacterSet = function () { + return defaultCharacterSet; + }; + /** + * Set the default character set. + * param {String} The character set. + */ + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; + }; +}; + +/** + * Get the raw DICOM data elements. + * @return {Object} The raw DICOM elements. + */ +dwv.dicom.DicomParser.prototype.getRawDicomElements = function () +{ + return this.dicomElements; +}; + +/** + * Get the DICOM data elements. + * @return {Object} The DICOM elements. + */ +dwv.dicom.DicomParser.prototype.getDicomElements = function () +{ + return new dwv.dicom.DicomElementsWrapper(this.dicomElements); +}; + +/** + * Read a DICOM tag. + * @param reader The raw data reader. + * @param offset The offset where to start to read. + * @return An object containing the tags 'group', 'element' and 'name'. + */ +dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) +{ + // group + var group = reader.readHex(offset); + offset += Uint16Array.BYTES_PER_ELEMENT; + // element + var element = reader.readHex(offset); + offset += Uint16Array.BYTES_PER_ELEMENT; + // name + var name = dwv.dicom.getGroupElementKey(group, element); + // return + return { + 'group': group, + 'element': element, + 'name': name, + 'endOffset': offset }; +}; + +/** + * Read an item data element. + * @param {Object} reader The raw data reader. + * @param {Number} offset The offset where to start to read. + * @param {Boolean} implicit Is the DICOM VR implicit? + * @returns {Object} The item data as a list of data elements. + */ +dwv.dicom.DicomParser.prototype.readItemDataElement = function (reader, offset, implicit) +{ + var itemData = {}; + + // read the first item + var item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + + // exit if it is a sequence delimitation item + var isSeqDelim = ( item.tag.name === "xFFFEE0DD" ); + if (isSeqDelim) { + return { + data: itemData, + endOffset: item.endOffset, + isSeqDelim: isSeqDelim }; + } + + // store it + itemData[item.tag.name] = item; + + // explicit VR items + if (item.vl !== "u/l") { + // not empty + if (item.vl !== 0) { + // read until the end offset + var endOffset = offset; + offset -= item.vl; + while (offset < endOffset) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + itemData[item.tag.name] = item; + } + } + } + // implicit VR items + else { + // read until the item delimitation item + var isItemDelim = false; + while (!isItemDelim) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + isItemDelim = ( item.tag.name === "xFFFEE00D" ); + if (!isItemDelim) { + itemData[item.tag.name] = item; + } + } + } + + return { + 'data': itemData, + 'endOffset': offset, + 'isSeqDelim': false }; +}; + +/** + * Read the pixel item data element. + * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}. + * @param {Object} reader The raw data reader. + * @param {Number} offset The offset where to start to read. + * @param {Boolean} implicit Is the DICOM VR implicit? + * @returns {Array} The item data as an array of data elements. + */ +dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function (reader, offset, implicit) +{ + var itemData = []; + + // first item: basic offset table + var item = this.readDataElement(reader, offset, implicit); + var offsetTableVl = item.vl; + offset = item.endOffset; + + // read until the sequence delimitation item + var isSeqDelim = false; + while (!isSeqDelim) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + isSeqDelim = ( item.tag.name === "xFFFEE0DD" ); + if (!isSeqDelim) { + itemData.push(item.value); + } + } + + return { + 'data': itemData, + 'endOffset': offset, + 'offsetTableVl': offsetTableVl }; +}; + +/** + * Read a DICOM data element. + * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}. + * @param {Object} reader The raw data reader. + * @param {Number} offset The offset where to start to read. + * @param {Boolean} implicit Is the DICOM VR implicit? + * @return {Object} An object containing the element 'tag', 'vl', 'vr', 'data' and 'endOffset'. + */ +dwv.dicom.DicomParser.prototype.readDataElement = function (reader, offset, implicit) +{ + // Tag: group, element + var tag = this.readTag(reader, offset); + offset = tag.endOffset; + + // Value Representation (VR) + var vr = null; + var is32bitVLVR = false; + if (dwv.dicom.isTagWithVR(tag.group, tag.element)) { + // implicit VR + if (implicit) { + vr = "UN"; + var dict = dwv.dicom.dictionary; + if ( typeof dict[tag.group] !== "undefined" && + typeof dict[tag.group][tag.element] !== "undefined" ) { + vr = dwv.dicom.dictionary[tag.group][tag.element][0]; + } + is32bitVLVR = true; + } + else { + vr = reader.readString( offset, 2 ); + offset += 2 * Uint8Array.BYTES_PER_ELEMENT; + is32bitVLVR = dwv.dicom.is32bitVLVR(vr); + // reserved 2 bytes + if ( is32bitVLVR ) { + offset += 2 * Uint8Array.BYTES_PER_ELEMENT; + } + } + } + else { + vr = "UN"; + is32bitVLVR = true; + } + + // Value Length (VL) + var vl = 0; + if ( is32bitVLVR ) { + vl = reader.readUint32( offset ); + offset += Uint32Array.BYTES_PER_ELEMENT; + } + else { + vl = reader.readUint16( offset ); + offset += Uint16Array.BYTES_PER_ELEMENT; + } + + // check the value of VL + var vlString = vl; + if( vl === 0xffffffff ) { + vlString = "u/l"; + vl = 0; + } + + var startOffset = offset; + + // data + var data = null; + var isPixelData = (tag.name === "x7FE00010"); + // pixel data sequence (implicit) + if (isPixelData && vlString === "u/l") + { + var pixItemData = this.readPixelItemDataElement(reader, offset, implicit); + offset = pixItemData.endOffset; + startOffset += pixItemData.offsetTableVl; + data = pixItemData.data; + } + else if (isPixelData && (vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox")) { + // BitsAllocated + var bitsAllocated = 16; + if ( typeof this.dicomElements.x00280100 !== 'undefined' ) { + bitsAllocated = this.dicomElements.x00280100.value[0]; + } else { + console.warn("Reading DICOM pixel data with default bitsAllocated."); + } + if (bitsAllocated === 8 && vr === "OW") { + console.warn("Reading DICOM pixel data with vr=OW and bitsAllocated=8 (should be 16)."); + } + if (bitsAllocated === 16 && vr === "OB") { + console.warn("Reading DICOM pixel data with vr=OB and bitsAllocated=16 (should be 8)."); + } + // PixelRepresentation 0->unsigned, 1->signed + var pixelRepresentation = 0; + if ( typeof this.dicomElements.x00280103 !== 'undefined' ) { + pixelRepresentation = this.dicomElements.x00280103.value[0]; + } + // read + if ( bitsAllocated === 8 ) { + if (pixelRepresentation === 0) { + data = reader.readUint8Array( offset, vl ); + } + else { + data = reader.readInt8Array( offset, vl ); + } + } + else if ( bitsAllocated === 16 ) { + if (pixelRepresentation === 0) { + data = reader.readUint16Array( offset, vl ); + } + else { + data = reader.readInt16Array( offset, vl ); + } + } + else if ( bitsAllocated === 32 ) { + if (pixelRepresentation === 0) { + data = reader.readUint32Array( offset, vl ); + } + else { + data = reader.readInt32Array( offset, vl ); + } + } + else if ( bitsAllocated === 64 ) { + if (pixelRepresentation === 0) { + data = reader.readUint64Array( offset, vl ); + } + else { + data = reader.readInt64Array( offset, vl ); + } + } + offset += vl; + } + // others + else if ( vr === "OB" ) + { + data = reader.readInt8Array( offset, vl ); + offset += vl; + } + else if ( vr === "OW" ) + { + data = reader.readInt16Array( offset, vl ); + offset += vl; + } + else if ( vr === "OF" ) + { + data = reader.readInt32Array( offset, vl ); + offset += vl; + } + else if ( vr === "OD" ) + { + data = reader.readInt64Array( offset, vl ); + offset += vl; + } + // numbers + else if( vr === "US") + { + data = reader.readUint16Array( offset, vl ); + offset += vl; + } + else if( vr === "UL") + { + data = reader.readUint32Array( offset, vl ); + offset += vl; + } + else if( vr === "SS") + { + data = reader.readInt16Array( offset, vl ); + offset += vl; + } + else if( vr === "SL") + { + data = reader.readInt32Array( offset, vl ); + offset += vl; + } + else if( vr === "FL") + { + data = reader.readFloat32Array( offset, vl ); + offset += vl; + } + else if( vr === "FD") + { + data = reader.readFloat64Array( offset, vl ); + offset += vl; + } + // attribute + else if( vr === "AT") + { + var raw = reader.readUint16Array( offset, vl ); + offset += vl; + data = []; + for ( var i = 0, leni = raw.length; i < leni; i+=2 ) { + var stri = raw[i].toString(16); + var stri1 = raw[i+1].toString(16); + var str = "("; + str += "0000".substr(0, 4 - stri.length) + stri.toUpperCase(); + str += ","; + str += "0000".substr(0, 4 - stri1.length) + stri1.toUpperCase(); + str += ")"; + data.push(str); + } + } + // not available + else if( vr === "UN") + { + data = reader.readUint8Array( offset, vl ); + offset += vl; + } + // sequence + else if (vr === "SQ") + { + data = []; + var itemData; + // explicit VR sequence + if (vlString !== "u/l") { + // not empty + if (vl !== 0) { + var sqEndOffset = offset + vl; + while (offset < sqEndOffset) { + itemData = this.readItemDataElement(reader, offset, implicit); + data.push( itemData.data ); + offset = itemData.endOffset; + } + } + } + // implicit VR sequence + else { + // read until the sequence delimitation item + var isSeqDelim = false; + while (!isSeqDelim) { + itemData = this.readItemDataElement(reader, offset, implicit); + isSeqDelim = itemData.isSeqDelim; + offset = itemData.endOffset; + // do not store the delimitation item + if (!isSeqDelim) { + data.push( itemData.data ); + } + } + } + } + // raw + else + { + if ( vr === "SH" || vr === "LO" || vr === "ST" || + vr === "PN" || vr === "LT" || vr === "UT" ) { + data = reader.readSpecialString( offset, vl ); + } else { + data = reader.readString( offset, vl ); + } + offset += vl; + data = data.split("\\"); + } + + // return + return { + 'tag': tag, + 'vr': vr, + 'vl': vlString, + 'value': data, + 'startOffset': startOffset, + 'endOffset': offset + }; +}; + +/** + * Parse the complete DICOM file (given as input to the class). + * Fills in the member object 'dicomElements'. + * @param buffer The input array buffer. + */ +dwv.dicom.DicomParser.prototype.parse = function (buffer) +{ + var offset = 0; + var implicit = false; + // default readers + var metaReader = new dwv.dicom.DataReader(buffer); + var dataReader = new dwv.dicom.DataReader(buffer); + + // 128 -> 132: magic word + offset = 128; + var magicword = metaReader.readString( offset, 4 ); + offset += 4 * Uint8Array.BYTES_PER_ELEMENT; + if(magicword !== "DICM") + { + throw new Error("Not a valid DICOM file (no magic DICM word found)"); + } + + // 0x0002, 0x0000: FileMetaInformationGroupLength + var dataElement = this.readDataElement(metaReader, offset, false); + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + // get meta length + var metaLength = parseInt(dataElement.value[0], 10); + + // meta elements + var metaEnd = offset + metaLength; + while( offset < metaEnd ) + { + // get the data element + dataElement = this.readDataElement(metaReader, offset, false); + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + } + + // check the TransferSyntaxUID (has to be there!) + if (typeof this.dicomElements.x00020010 === "undefined") + { + throw new Error("Not a valid DICOM file (no TransferSyntaxUID found)"); + } + var syntax = dwv.dicom.cleanString(this.dicomElements.x00020010.value[0]); + + // check support + if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) { + throw new Error("Unsupported DICOM transfer syntax: '"+syntax+ + "' ("+dwv.dicom.getTransferSyntaxName(syntax)+")"); + } + + // Implicit VR + if (dwv.dicom.isImplicitTransferSyntax(syntax)) { + implicit = true; + } + + // Big Endian + if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { + dataReader = new dwv.dicom.DataReader(buffer,false); + } + + // default character set + if (typeof this.getDefaultCharacterSet() !== "undefined") { + dataReader.setUtfLabel(this.getDefaultCharacterSet()); + } + + // DICOM data elements + while ( offset < buffer.byteLength ) + { + // get the data element + dataElement = this.readDataElement(dataReader, offset, implicit); + // check character set + if (dataElement.tag.name === "x00080005") { + var charSetTerm; + if (dataElement.value.length === 1) { + charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); + } + else { + charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); + console.warn("Unsupported character set with code extensions: '"+charSetTerm+"'."); + } + dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); + } + // increment offset + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + } + + // safety check... + if (buffer.byteLength !== offset) { + console.warn("Did not reach the end of the buffer: "+ + offset+" != "+buffer.byteLength); + } + + // pixel buffer + if (typeof this.dicomElements.x7FE00010 !== "undefined") { + + var numberOfFrames = 1; + if (typeof this.dicomElements.x00280008 !== "undefined") { + numberOfFrames = this.dicomElements.x00280008.value[0]; + } + + if (this.dicomElements.x7FE00010.vl !== "u/l") { + // compressed should be encapsulated... + if (dwv.dicom.isJpeg2000TransferSyntax( syntax ) || + dwv.dicom.isJpegBaselineTransferSyntax( syntax ) || + dwv.dicom.isJpegLosslessTransferSyntax( syntax ) ) { + console.warn("Compressed but no items..."); + } + + // calculate the slice size + var pixData = this.dicomElements.x7FE00010.value; + var columns = this.dicomElements.x00280011.value[0]; + var rows = this.dicomElements.x00280010.value[0]; + var samplesPerPixel = this.dicomElements.x00280002.value[0]; + var sliceSize = columns * rows * samplesPerPixel; + // slice data in an array of frames + var newPixData = []; + var frameOffset = 0; + for (var g = 0; g < numberOfFrames; ++g) { + newPixData[g] = pixData.slice(frameOffset, frameOffset+sliceSize); + frameOffset += sliceSize; + } + // store as pixel data + this.dicomElements.x7FE00010.value = newPixData; + } + else { + // handle fragmented pixel buffer + // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html + // (third note, "Depending on the transfer syntax...") + var pixItems = this.dicomElements.x7FE00010.value; + if (pixItems.length > 1 && pixItems.length > numberOfFrames ) { + + // concatenate pixel data items + // concat does not work on typed arrays + //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); + // manual concat... + var nItemPerFrame = pixItems.length / numberOfFrames; + var newPixItems = []; + var index = 0; + for (var f = 0; f < numberOfFrames; ++f) { + index = f * nItemPerFrame; + // calculate the size of a frame + var size = 0; + for (var i = 0; i < nItemPerFrame; ++i) { + size += pixItems[index + i].length; + } + // create new buffer + var newBuffer = new pixItems[0].constructor(size); + // fill new buffer + var fragOffset = 0; + for (var j = 0; j < nItemPerFrame; ++j) { + newBuffer.set( pixItems[index + j], fragOffset ); + fragOffset += pixItems[index + j].length; + } + newPixItems[f] = newBuffer; + } + // store as pixel data + this.dicomElements.x7FE00010.value = newPixItems; + } + } + } +}; + +/** + * DicomElements wrapper. + * @constructor + * @param {Array} dicomElements The elements to wrap. + */ +dwv.dicom.DicomElementsWrapper = function (dicomElements) { + + /** + * Get a DICOM Element value from a group/element key. + * @param {String} groupElementKey The key to retrieve. + * @return {Object} The DICOM element. + */ + this.getDEFromKey = function ( groupElementKey ) { + return dicomElements[groupElementKey]; + }; + + /** + * Get a DICOM Element value from a group/element key. + * @param {String} groupElementKey The key to retrieve. + * @param {Boolean} asArray Get the value as an Array. + * @return {Object} The DICOM element value. + */ + this.getFromKey = function ( groupElementKey, asArray ) { + // default + if ( typeof asArray === "undefined" ) { + asArray = false; + } + var value = null; + var dElement = dicomElements[groupElementKey]; + if ( typeof dElement !== "undefined" ) { + // raw value if only one + if ( dElement.value.length === 1 && asArray === false) { + value = dElement.value[0]; + } + else { + value = dElement.value; + } + } + return value; + }; + + /** + * Dump the DICOM tags to an array. + * @return {Array} + */ + this.dumpToTable = function () { + var keys = Object.keys(dicomElements); + var dict = dwv.dicom.dictionary; + var table = []; + var dicomElement = null; + var dictElement = null; + var row = null; + for ( var i = 0, leni = keys.length; i < leni; ++i ) { + dicomElement = dicomElements[keys[i]]; + row = {}; + // dictionnary entry (to get name) + dictElement = null; + if ( typeof dict[dicomElement.tag.group] !== "undefined" && + typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") { + dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; + } + // name + if ( dictElement !== null ) { + row.name = dictElement[2]; + } + else { + row.name = "Unknown Tag & Data"; + } + // value + row.value = this.getElementValueAsString(dicomElement); + // others + row.group = dicomElement.tag.group; + row.element = dicomElement.tag.element; + row.vr = dicomElement.vr; + row.vl = dicomElement.vl; + + table.push( row ); + } + return table; + }; + + /** + * Dump the DICOM tags to a string. + * @return {String} The dumped file. + */ + this.dump = function () { + var keys = Object.keys(dicomElements); + var result = "\n"; + result += "# Dicom-File-Format\n"; + result += "\n"; + result += "# Dicom-Meta-Information-Header\n"; + result += "# Used TransferSyntax: "; + if ( dwv.dicom.isNativeLittleEndian() ) { + result += "Little Endian Explicit\n"; + } + else { + result += "NOT Little Endian Explicit\n"; + } + var dicomElement = null; + var checkHeader = true; + for ( var i = 0, leni = keys.length; i < leni; ++i ) { + dicomElement = dicomElements[keys[i]]; + if ( checkHeader && dicomElement.tag.group !== "0x0002" ) { + result += "\n"; + result += "# Dicom-Data-Set\n"; + result += "# Used TransferSyntax: "; + var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]); + result += dwv.dicom.getTransferSyntaxName(syntax); + result += "\n"; + checkHeader = false; + } + result += this.getElementAsString(dicomElement) + "\n"; + } + return result; + }; + +}; + +/** + * Get a data element value as a string. + * @param {Object} dicomElement The DICOM element. + * @param {Boolean} pretty When set to true, returns a 'pretified' content. + * @return {String} A string representation of the DICOM element. + */ +dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( dicomElement, pretty ) +{ + var str = ""; + var strLenLimit = 65; + + // dafault to pretty output + if ( typeof pretty === "undefined" ) { + pretty = true; + } + // check dicom element input + if ( typeof dicomElement === "undefined" || dicomElement === null ) { + return str; + } + + // Polyfill for Number.isInteger. + var isInteger = Number.isInteger || function (value) { + return typeof value === 'number' && + isFinite(value) && + Math.floor(value) === value; + }; + + // TODO Support sequences. + + if ( dicomElement.vr !== "SQ" && + dicomElement.value.length === 1 && dicomElement.value[0] === "" ) { + str += "(no value available)"; + } else if ( dicomElement.tag.group === '0x7FE0' && + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l' ) { + str = "(PixelSequence)"; + } else if ( dicomElement.vr === "DA" && pretty ) { + var daValue = dicomElement.value[0]; + var daYear = parseInt( daValue.substr(0,4), 10 ); + var daMonth = parseInt( daValue.substr(4,2), 10 ) - 1; // 0-11 + var daDay = parseInt( daValue.substr(6,2), 10 ); + var da = new Date(daYear, daMonth, daDay); + str = da.toLocaleDateString(); + } else if ( dicomElement.vr === "TM" && pretty ) { + var tmValue = dicomElement.value[0]; + var tmHour = tmValue.substr(0,2); + var tmMinute = tmValue.length >= 4 ? tmValue.substr(2,2) : "00"; + var tmSeconds = tmValue.length >= 6 ? tmValue.substr(4,2) : "00"; + str = tmHour + ':' + tmMinute + ':' + tmSeconds; + } else { + var isOtherVR = ( dicomElement.vr[0].toUpperCase() === "O" ); + var isFloatNumberVR = ( dicomElement.vr === "FL" || + dicomElement.vr === "FD" || + dicomElement.vr === "DS"); + var valueStr = ""; + for ( var k = 0, lenk = dicomElement.value.length; k < lenk; ++k ) { + valueStr = ""; + if ( k !== 0 ) { + valueStr += "\\"; + } + if ( isFloatNumberVR ) { + var val = dicomElement.value[k]; + if (typeof val === "string") { + val = dwv.dicom.cleanString(val); + } + var num = Number( val ); + if ( !isInteger( num ) && pretty ) { + valueStr += num.toPrecision(4); + } else { + valueStr += num.toString(); + } + } else if ( isOtherVR ) { + var tmp = dicomElement.value[k].toString(16); + if ( dicomElement.vr === "OB" ) { + tmp = "00".substr(0, 2 - tmp.length) + tmp; + } + else { + tmp = "0000".substr(0, 4 - tmp.length) + tmp; + } + valueStr += tmp; + } else if ( typeof dicomElement.value[k] === "string" ) { + valueStr += dwv.dicom.cleanString(dicomElement.value[k]); + } else { + valueStr += dicomElement.value[k]; + } + // check length + if ( str.length + valueStr.length <= strLenLimit ) { + str += valueStr; + } else { + str += "..."; + break; + } + } + } + return str; +}; + +/** + * Get a data element value as a string. + * @param {String} groupElementKey The key to retrieve. + */ +dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsStringFromKey = function ( groupElementKey ) +{ + return this.getElementValueAsString( this.getDEFromKey(groupElementKey) ); +}; + +/** + * Get a data element as a string. + * @param {Object} dicomElement The DICOM element. + * @param {String} prefix A string to prepend this one. + */ +dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( dicomElement, prefix ) +{ + // default prefix + prefix = prefix || ""; + + // get element from dictionary + var dict = dwv.dicom.dictionary; + var dictElement = null; + if ( typeof dict[dicomElement.tag.group] !== "undefined" && + typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") { + dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; + } + + var deSize = dicomElement.value.length; + var isOtherVR = ( dicomElement.vr[0].toUpperCase() === "O" ); + + // no size for delimitations + if ( dicomElement.tag.group === "0xFFFE" && ( + dicomElement.tag.element === "0xE00D" || + dicomElement.tag.element === "0xE0DD" ) ) { + deSize = 0; + } + else if ( isOtherVR ) { + deSize = 1; + } + + var isPixSequence = (dicomElement.tag.group === '0x7FE0' && + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l'); + + var line = null; + + // (group,element) + line = "("; + line += dicomElement.tag.group.substr(2,5).toLowerCase(); + line += ","; + line += dicomElement.tag.element.substr(2,5).toLowerCase(); + line += ") "; + // value representation + line += dicomElement.vr; + // value + if ( dicomElement.vr !== "SQ" && dicomElement.value.length === 1 && dicomElement.value[0] === "" ) { + line += " (no value available)"; + deSize = 0; + } + else { + // simple number display + if ( dicomElement.vr === "na" ) { + line += " "; + line += dicomElement.value[0]; + } + // pixel sequence + else if ( isPixSequence ) { + line += " (PixelSequence #=" + deSize + ")"; + } + else if ( dicomElement.vr === 'SQ' ) { + line += " (Sequence with"; + if ( dicomElement.vl === "u/l" ) { + line += " undefined"; + } + else { + line += " explicit"; + } + line += " length #="; + line += dicomElement.value.length; + line += ")"; + } + // 'O'ther array, limited display length + else if ( isOtherVR || + dicomElement.vr === 'pi' || + dicomElement.vr === "UL" || + dicomElement.vr === "US" || + dicomElement.vr === "SL" || + dicomElement.vr === "SS" || + dicomElement.vr === "FL" || + dicomElement.vr === "FD" || + dicomElement.vr === "AT" ) { + line += " "; + line += this.getElementValueAsString(dicomElement, false); + } + // default + else { + line += " ["; + line += this.getElementValueAsString(dicomElement, false); + line += "]"; + } + } + + // align # + var nSpaces = 55 - line.length; + if ( nSpaces > 0 ) { + for ( var s = 0; s < nSpaces; ++s ) { + line += " "; + } + } + line += " # "; + if ( dicomElement.vl < 100 ) { + line += " "; + } + if ( dicomElement.vl < 10 ) { + line += " "; + } + line += dicomElement.vl; + line += ", "; + line += deSize; //dictElement[1]; + line += " "; + if ( dictElement !== null ) { + line += dictElement[2]; + } + else { + line += "Unknown Tag & Data"; + } + + var message = null; + + // continue for sequence + if ( dicomElement.vr === 'SQ' ) { + var item = null; + for ( var l = 0, lenl = dicomElement.value.length; l < lenl; ++l ) { + item = dicomElement.value[l]; + var itemKeys = Object.keys(item); + if ( itemKeys.length === 0 ) { + continue; + } + + // get the item element + var itemElement = item.xFFFEE000; + message = "(Item with"; + if ( itemElement.vl === "u/l" ) { + message += " undefined"; + } + else { + message += " explicit"; + } + message += " length #="+(itemKeys.length - 1)+")"; + itemElement.value = [message]; + itemElement.vr = "na"; + + line += "\n"; + line += this.getElementAsString(itemElement, prefix + " "); + + for ( var m = 0, lenm = itemKeys.length; m < lenm; ++m ) { + if ( itemKeys[m] !== "xFFFEE000" ) { + line += "\n"; + line += this.getElementAsString(item[itemKeys[m]], prefix + " "); + } + } + + message = "(ItemDelimitationItem"; + if ( itemElement.vl !== "u/l" ) { + message += " for re-encoding"; + } + message += ")"; + var itemDelimElement = { + "tag": { "group": "0xFFFE", "element": "0xE00D" }, + "vr": "na", + "vl": "0", + "value": [message] + }; + line += "\n"; + line += this.getElementAsString(itemDelimElement, prefix + " "); + + } + + message = "(SequenceDelimitationItem"; + if ( dicomElement.vl !== "u/l" ) { + message += " for re-encod."; + } + message += ")"; + var sqDelimElement = { + "tag": { "group": "0xFFFE", "element": "0xE0DD" }, + "vr": "na", + "vl": "0", + "value": [message] + }; + line += "\n"; + line += this.getElementAsString(sqDelimElement, prefix); + } + // pixel sequence + else if ( isPixSequence ) { + var pixItem = null; + for ( var n = 0, lenn = dicomElement.value.length; n < lenn; ++n ) { + pixItem = dicomElement.value[n]; + line += "\n"; + pixItem.vr = 'pi'; + line += this.getElementAsString(pixItem, prefix + " "); + } + + var pixDelimElement = { + "tag": { "group": "0xFFFE", "element": "0xE0DD" }, + "vr": "na", + "vl": "0", + "value": ["(SequenceDelimitationItem)"] + }; + line += "\n"; + line += this.getElementAsString(pixDelimElement, prefix); + } + + return prefix + line; +}; + +/** + * Get a DICOM Element value from a group and an element. + * @param {Number} group The group. + * @param {Number} element The element. + * @return {Object} The DICOM element value. + */ +dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( + group, element ) +{ + return this.getFromKey( + dwv.dicom.getGroupElementKey(group, element) ); +}; + +/** + * Get a DICOM Element value from a tag name. + * Uses the DICOM dictionary. + * @param {String} name The tag name. + * @return {Object} The DICOM element value. + */ +dwv.dicom.DicomElementsWrapper.prototype.getFromName = function ( name ) +{ + var value = null; + var tagGE = dwv.dicom.getGroupElementFromName(name); + // check that we are not at the end of the dictionary + if ( tagGE.group !== null && tagGE.element !== null ) { + value = this.getFromKey(dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element)); + } + return value; +}; + +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + +/** + * Data writer. + * + * Example usage: + * var parser = new dwv.dicom.DicomParser(); + * parser.parse(this.response); + * + * var writer = new dwv.dicom.DicomWriter(parser.getRawDicomElements()); + * var blob = new Blob([writer.getBuffer()], {type: 'application/dicom'}); + * + * var element = document.getElementById("download"); + * element.href = URL.createObjectURL(blob); + * element.download = "anonym.dcm"; + * + * @constructor + * @param {Array} buffer The input array buffer. + * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian. + */ +dwv.dicom.DataWriter = function (buffer, isLittleEndian) +{ + // Set endian flag if not defined. + if ( typeof isLittleEndian === 'undefined' ) { + isLittleEndian = true; + } + + // private DataView + var view = new DataView(buffer); + + /** + * Write Uint8 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeUint8 = function (byteOffset, value) { + view.setUint8(byteOffset, value); + return byteOffset + Uint8Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int8 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeInt8 = function (byteOffset, value) { + view.setInt8(byteOffset, value); + return byteOffset + Int8Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Uint16 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeUint16 = function (byteOffset, value) { + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int16 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeInt16 = function (byteOffset, value) { + view.setInt16(byteOffset, value, isLittleEndian); + return byteOffset + Int16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Uint32 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeUint32 = function (byteOffset, value) { + view.setUint32(byteOffset, value, isLittleEndian); + return byteOffset + Uint32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int32 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeInt32 = function (byteOffset, value) { + view.setInt32(byteOffset, value, isLittleEndian); + return byteOffset + Int32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Float32 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeFloat32 = function (byteOffset, value) { + view.setFloat32(byteOffset, value, isLittleEndian); + return byteOffset + Float32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Float64 data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} value The data to write. + * @returns {Number} The new offset position. + */ + this.writeFloat64 = function (byteOffset, value) { + view.setFloat64(byteOffset, value, isLittleEndian); + return byteOffset + Float64Array.BYTES_PER_ELEMENT; + }; + + /** + * Write string data as hexadecimal. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} str The padded hexadecimal string to write ('0x####'). + * @returns {Number} The new offset position. + */ + this.writeHex = function (byteOffset, str) { + // remove first two chars and parse + var value = parseInt(str.substr(2), 16); + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write string data. + * @param {Number} byteOffset The offset to start writing from. + * @param {Number} str The data to write. + * @returns {Number} The new offset position. + */ + this.writeString = function (byteOffset, str) { + for ( var i = 0, len = str.length; i < len; ++i ) { + view.setUint8(byteOffset, str.charCodeAt(i)); + byteOffset += Uint8Array.BYTES_PER_ELEMENT; + } + return byteOffset; + }; + +}; + +/** + * Write Uint8 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint8Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeUint8(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int8 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt8Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeInt8(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Uint16 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint16Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeUint16(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int16 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt16Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeInt16(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Uint32 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint32Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeUint32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int32 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt32Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeInt32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Float32 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat32Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeFloat32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Float64 array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat64Array = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + byteOffset = this.writeFloat64(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write string array. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeStringArray = function (byteOffset, array) { + for ( var i = 0, len = array.length; i < len; ++i ) { + // separator + if ( i !== 0 ) { + byteOffset = this.writeString(byteOffset, "\\"); + } + // value + byteOffset = this.writeString(byteOffset, array[i].toString()); + } + return byteOffset; +}; + +/** + * Write a list of items. + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} items The list of items to write. + * @param {Boolean} isImplicit Is the DICOM VR implicit? + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeDataElementItems = function (byteOffset, items, isImplicit) { + var item = null; + for ( var i = 0; i < items.length; ++i ) { + item = items[i]; + var itemKeys = Object.keys(item); + if ( itemKeys.length === 0 ) { + continue; + } + // write item + var itemElement = item.xFFFEE000; + itemElement.value = []; + var implicitLength = (itemElement.vl === "u/l"); + if (implicitLength) { + itemElement.vl = 0xffffffff; + } + byteOffset = this.writeDataElement(itemElement, byteOffset, isImplicit); + // write rest + for ( var m = 0; m < itemKeys.length; ++m ) { + if ( itemKeys[m] !== "xFFFEE000" && itemKeys[m] !== "xFFFEE00D") { + byteOffset = this.writeDataElement(item[itemKeys[m]], byteOffset, isImplicit); + } + } + // item delimitation + if (implicitLength) { + var itemDelimElement = { + 'tag': { group: "0xFFFE", + element: "0xE00D", + name: "ItemDelimitationItem" }, + 'vr': "NONE", + 'vl': 0, + 'value': [] + }; + byteOffset = this.writeDataElement(itemDelimElement, byteOffset, isImplicit); + } + } + + // return new offset + return byteOffset; +}; + +/** + * Write data with a specific Value Representation (VR). + * @param {String} vr The data Value Representation (VR). + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} value The array to write. + * @param {Boolean} isImplicit Is the DICOM VR implicit? + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeDataElementValue = function (vr, byteOffset, value, isImplicit) { + // first check input type to know how to write + if (value instanceof Uint8Array) { + byteOffset = this.writeUint8Array(byteOffset, value); + } else if (value instanceof Int8Array) { + byteOffset = this.writeInt8Array(byteOffset, value); + } else if (value instanceof Uint16Array) { + byteOffset = this.writeUint16Array(byteOffset, value); + } else if (value instanceof Int16Array) { + byteOffset = this.writeInt16Array(byteOffset, value); + } else if (value instanceof Uint32Array) { + byteOffset = this.writeUint32Array(byteOffset, value); + } else if (value instanceof Int32Array) { + byteOffset = this.writeInt32Array(byteOffset, value); + } else { + // switch according to VR if input type is undefined + if ( vr === "UN" ) { + byteOffset = this.writeUint8Array(byteOffset, value); + } else if ( vr === "OB" ) { + byteOffset = this.writeInt8Array(byteOffset, value); + } else if ( vr === "OW" ) { + byteOffset = this.writeInt16Array(byteOffset, value); + } else if ( vr === "OF" ) { + byteOffset = this.writeInt32Array(byteOffset, value); + } else if ( vr === "OD" ) { + byteOffset = this.writeInt64Array(byteOffset, value); + } else if ( vr === "US") { + byteOffset = this.writeUint16Array(byteOffset, value); + } else if ( vr === "SS") { + byteOffset = this.writeInt16Array(byteOffset, value); + } else if ( vr === "UL") { + byteOffset = this.writeUint32Array(byteOffset, value); + } else if ( vr === "SL") { + byteOffset = this.writeInt32Array(byteOffset, value); + } else if ( vr === "FL") { + byteOffset = this.writeFloat32Array(byteOffset, value); + } else if ( vr === "FD") { + byteOffset = this.writeFloat64Array(byteOffset, value); + } else if ( vr === "SQ") { + byteOffset = this.writeDataElementItems(byteOffset, value, isImplicit); + } else if ( vr === "AT") { + for ( var i = 0; i < value.length; ++i ) { + var hexString = value[i] + ''; + var hexString1 = hexString.substring(1, 5); + var hexString2 = hexString.substring(6, 10); + var dec1 = parseInt(hexString1, 16); + var dec2 = parseInt(hexString2, 16); + var atValue = new Uint16Array([dec1, dec2]); + byteOffset = this.writeUint16Array(byteOffset, atValue); + } + } else { + byteOffset = this.writeStringArray(byteOffset, value); + } + } + // return new offset + return byteOffset; +}; + +/** + * Write a pixel data element. + * @param {String} vr The data Value Representation (VR). + * @param {String} vl The data Value Length (VL). + * @param {Number} byteOffset The offset to start writing from. + * @param {Array} value The array to write. + * @param {Boolean} isImplicit Is the DICOM VR implicit? + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function (vr, vl, byteOffset, value, isImplicit) { + // explicit length + if (vl !== "u/l") { + var finalValue = value[0]; + // flatten multi frame + if (value.length > 1) { + finalValue = dwv.dicom.flattenArrayOfTypedArrays(value); + } + // write + byteOffset = this.writeDataElementValue(vr, byteOffset, finalValue, isImplicit); + } else { + // pixel data as sequence + var item = {}; + // first item: basic offset table + item.xFFFEE000 = { + 'tag': { group: "0xFFFE", + element: "0xE000", + name: "xFFFEE000" }, + 'vr': "UN", + 'vl': 0, + 'value': [] + }; + // data + for (var i = 0; i < value.length; ++i) { + item[i] = { + 'tag': { group: "0xFFFE", + element: "0xE000", + name: "xFFFEE000" }, + 'vr': vr, + 'vl': value[i].length, + 'value': value[i] + }; + } + // write + byteOffset = this.writeDataElementItems(byteOffset, [item], isImplicit); + } + + // return new offset + return byteOffset; +}; + +/** + * Write a data element. + * @param {Object} element The DICOM data element to write. + * @param {Number} byteOffset The offset to start writing from. + * @param {Boolean} isImplicit Is the DICOM VR implicit? + * @returns {Number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeDataElement = function (element, byteOffset, isImplicit) { + var isTagWithVR = dwv.dicom.isTagWithVR(element.tag.group, element.tag.element); + var is32bitVLVR = (isImplicit || !isTagWithVR) ? true : dwv.dicom.is32bitVLVR(element.vr); + // group + byteOffset = this.writeHex(byteOffset, element.tag.group); + // element + byteOffset = this.writeHex(byteOffset, element.tag.element); + // VR + if ( isTagWithVR && !isImplicit ) { + byteOffset = this.writeString(byteOffset, element.vr); + // reserved 2 bytes for 32bit VL + if ( is32bitVLVR ) { + byteOffset += 2; + } + } + + // update vl for sequence or item with implicit length + var vl = element.vl; + if ( dwv.dicom.isImplicitLengthSequence(element) || + dwv.dicom.isImplicitLengthItem(element) || + dwv.dicom.isImplicitLengthPixels(element) ) { + vl = 0xffffffff; + } + // VL + if ( is32bitVLVR ) { + byteOffset = this.writeUint32(byteOffset, vl); + } + else { + byteOffset = this.writeUint16(byteOffset, vl); + } + + // value + var value = element.value; + // check value + if (typeof value === 'undefined') { + value = []; + } + // write + if (element.tag.name === "x7FE00010") { + byteOffset = this.writePixelDataElementValue(element.vr, element.vl, byteOffset, value, isImplicit); + } else { + byteOffset = this.writeDataElementValue(element.vr, byteOffset, value, isImplicit); + } + + // sequence delimitation item for sequence with implicit length + if ( dwv.dicom.isImplicitLengthSequence(element) || + dwv.dicom.isImplicitLengthPixels(element) ) { + var seqDelimElement = { + 'tag': { group: "0xFFFE", + element: "0xE0DD", + name: "SequenceDelimitationItem" }, + 'vr': "NONE", + 'vl': 0, + 'value': [] + }; + byteOffset = this.writeDataElement(seqDelimElement, byteOffset, isImplicit); + } + + // return new offset + return byteOffset; +}; + +/** + * Is this element an implicit length sequence? + * @param {Object} element The element to check. + * @returns {Boolean} True if it is. + */ +dwv.dicom.isImplicitLengthSequence = function (element) { + // sequence with no length + return (element.vr === "SQ") && + (element.vl === "u/l"); +}; + +/** + * Is this element an implicit length item? + * @param {Object} element The element to check. + * @returns {Boolean} True if it is. + */ +dwv.dicom.isImplicitLengthItem = function (element) { + // item with no length + return (element.tag.name === "xFFFEE000") && + (element.vl === "u/l"); +}; + +/** + * Is this element an implicit length pixel data? + * @param {Object} element The element to check. + * @returns {Boolean} True if it is. + */ +dwv.dicom.isImplicitLengthPixels = function (element) { + // pixel data with no length + return (element.tag.name === "x7FE00010") && + (element.vl === "u/l"); +}; + +/** + * Helper method to flatten an array of typed arrays to 2D typed array + * @param {Array} array of typed arrays + * @returns {Object} a typed array containing all values + */ +dwv.dicom.flattenArrayOfTypedArrays = function(initialArray) { + var initialArrayLength = initialArray.length; + var arrayLength = initialArray[0].length; + var flattenendArrayLength = initialArrayLength * arrayLength; + + var flattenedArray = new initialArray[0].constructor(flattenendArrayLength); + + for (var i = 0; i < initialArrayLength; i++) { + var indexFlattenedArray = i * arrayLength; + flattenedArray.set(initialArray[i], indexFlattenedArray); + } + return flattenedArray; +}; + +/** + * DICOM writer. + * @constructor + */ +dwv.dicom.DicomWriter = function () { + + // possible tag actions + var actions = { + 'copy': function (item) { return item; }, + 'remove': function () { return null; }, + 'clear': function (item) { + item.value[0] = ""; + item.vl = 0; + item.endOffset = item.startOffset; + return item; + }, + 'replace': function (item, value) { + item.value[0] = value; + item.vl = value.length; + item.endOffset = item.startOffset + value.length; + return item; + } + }; + + // default rules: just copy + var defaultRules = { + 'default': {action: 'copy', value: null } + }; + + /** + * Public (modifiable) rules. + * Set of objects as: + * name : { action: 'actionName', value: 'optionalValue } + * The names are either 'default', tagName or groupName. + * Each DICOM element will be checked to see if a rule is applicable. + * First checked by tagName and then by groupName, + * if nothing is found the default rule is applied. + */ + this.rules = defaultRules; + + /** + * Example anonymisation rules. + */ + this.anonymisationRules = { + 'default': {action: 'remove', value: null }, + 'PatientName': {action: 'replace', value: 'Anonymized'}, // tag + 'Meta Element' : {action: 'copy', value: null }, // group 'x0002' + 'Acquisition' : {action: 'copy', value: null }, // group 'x0018' + 'Image Presentation' : {action: 'copy', value: null }, // group 'x0028' + 'Procedure' : {action: 'copy', value: null }, // group 'x0040' + 'Pixel Data' : {action: 'copy', value: null } // group 'x7fe0' + }; + + /** + * Get the element to write according to the class rules. + * Priority order: tagName, groupName, default. + * @param {Object} element The element to check + * @return {Object} The element to write, can be null. + */ + this.getElementToWrite = function (element) { + // get group and tag string name + var tagName = null; + var dict = dwv.dicom.dictionary; + var group = element.tag.group; + var groupName = dwv.dicom.TagGroups[group.substr(1)]; // remove first 0 + + if ( typeof dict[group] !== 'undefined' && typeof dict[group][element.tag.element] !== 'undefined') { + tagName = dict[group][element.tag.element][2]; + } + // apply rules: + var rule; + // 1. tag itself + if (typeof this.rules[element.tag.name] !== 'undefined') { + rule = this.rules[element.tag.name]; + } + // 2. tag name + else if ( tagName !== null && typeof this.rules[tagName] !== 'undefined' ) { + rule = this.rules[tagName]; + } + // 3. group name + else if ( typeof this.rules[groupName] !== 'undefined' ) { + rule = this.rules[groupName]; + } + // 4. default + else { + rule = this.rules['default']; + } + // apply action on element and return + return actions[rule.action](element, rule.value); + }; +}; + +/** + * Get the ArrayBuffer corresponding to input DICOM elements. + * @param {Array} dicomElements The wrapped elements to write. + * @returns {ArrayBuffer} The elements as a buffer. + */ +dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { + // array keys + var keys = Object.keys(dicomElements); + + // transfer syntax + var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]); + var isImplicit = dwv.dicom.isImplicitTransferSyntax(syntax); + var isBigEndian = dwv.dicom.isBigEndianTransferSyntax(syntax); + + // calculate buffer size and split elements (meta and non meta) + var totalSize = 128 + 4; // DICM + var localSize = 0; + var metaElements = []; + var rawElements = []; + var element; + var groupName; + var metaLength = 0; + var fmiglTag = dwv.dicom.getFileMetaInformationGroupLengthTag(); + var icUIDTag = new dwv.dicom.Tag("0x0002", "0x0012"); // ImplementationClassUID + var ivnTag = new dwv.dicom.Tag("0x0002", "0x0013"); // ImplementationVersionName + for ( var i = 0, leni = keys.length; i < leni; ++i ) { + element = this.getElementToWrite(dicomElements[keys[i]]); + if ( element !== null && + !fmiglTag.equals2(element.tag) && + !icUIDTag.equals2(element.tag) && + !ivnTag.equals2(element.tag) ) { + localSize = 0; + // tag group name + groupName = dwv.dicom.TagGroups[element.tag.group.substr(1)]; // remove first 0 + + // prefix + if ( groupName === 'Meta Element' ) { + localSize += dwv.dicom.getDataElementPrefixByteSize(element.vr, false); + } else { + localSize += dwv.dicom.getDataElementPrefixByteSize(element.vr, isImplicit); + } + + // value + var realVl = element.endOffset - element.startOffset; + localSize += parseInt(realVl, 10); + + // add size of sequence delimitation item + if ( dwv.dicom.isImplicitLengthSequence(element) || + dwv.dicom.isImplicitLengthPixels(element) ) { + localSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit); + } + + // sort elements + if ( groupName === 'Meta Element' ) { + metaElements.push(element); + metaLength += localSize; + } + else { + rawElements.push(element); + } + + // add to total size + totalSize += localSize; + } + } + + // ImplementationClassUID + var icUID = dwv.dicom.getDicomElement("ImplementationClassUID"); + var icUIDSize = dwv.dicom.getDataElementPrefixByteSize(icUID.vr, isImplicit); + icUIDSize += dwv.dicom.setElementValue(icUID, "1.2.826.0.1.3680043.9.7278.1."+dwv.getVersion(), false); + metaElements.push(icUID); + metaLength += icUIDSize; + totalSize += icUIDSize; + // ImplementationVersionName + var ivn = dwv.dicom.getDicomElement("ImplementationVersionName"); + var ivnSize = dwv.dicom.getDataElementPrefixByteSize(ivn.vr, isImplicit); + var ivnValue = "DWV_" + dwv.getVersion(); + // odd IDs should be padded + if ( ivnValue.length % 2 === 1 ) { + ivnValue += '\0'; + } + ivnSize += dwv.dicom.setElementValue(ivn, ivnValue, false); + metaElements.push(ivn); + metaLength += ivnSize; + totalSize += ivnSize; + + // create the FileMetaInformationGroupLength element + var fmigl = dwv.dicom.getDicomElement("FileMetaInformationGroupLength"); + var fmiglSize = dwv.dicom.getDataElementPrefixByteSize(fmigl.vr, isImplicit); + fmiglSize += dwv.dicom.setElementValue(fmigl, metaLength, false); + + // add its size to the total one + totalSize += fmiglSize; + + // create buffer + var buffer = new ArrayBuffer(totalSize); + var metaWriter = new dwv.dicom.DataWriter(buffer); + var dataWriter = new dwv.dicom.DataWriter(buffer, !isBigEndian); + var offset = 128; + // DICM + offset = metaWriter.writeString(offset, "DICM"); + // FileMetaInformationGroupLength + offset = metaWriter.writeDataElement(fmigl, offset, false); + // write meta + for ( var j = 0, lenj = metaElements.length; j < lenj; ++j ) { + offset = metaWriter.writeDataElement(metaElements[j], offset, false); + } + // write non meta + for ( var k = 0, lenk = rawElements.length; k < lenk; ++k ) { + offset = dataWriter.writeDataElement(rawElements[k], offset, isImplicit); + } + + // return + return buffer; +}; + +/** + * Get a DICOM element from its tag name (value set separatly). + * @param {String} tagName The string tag name. + * @return {Object} The DICOM element. + */ +dwv.dicom.getDicomElement = function (tagName) +{ + var tagGE = dwv.dicom.getGroupElementFromName(tagName); + var dict = dwv.dicom.dictionary; + // return element definition + return { + 'tag': { 'group': tagGE.group, 'element': tagGE.element }, + 'vr': dict[tagGE.group][tagGE.element][0], + 'vl': dict[tagGE.group][tagGE.element][1] + }; +}; + +/** + * Set a DICOM element value according to its VR (Value Representation). + * @param {Object} element The DICOM element to set the value. + * @param {Object} value The value to set. + * @param {Boolean} isImplicit Does the data use implicit VR? + * @return {Number} The total element size. + */ +dwv.dicom.setElementValue = function (element, value, isImplicit) { + // byte size of the element + var size = 0; + // special sequence case + if ( element.vr === "SQ") { + + // set the value + element.value = value; + element.vl = 0; + + if ( value !== null && value !== 0 ) { + var sqItems = []; + var name; + + // explicit or implicit length + var explicitLength = true; + if ( typeof value.explicitLength !== "undefined" ) { + explicitLength = value.explicitLength; + delete value.explicitLength; + } + + // items + var itemData; + var itemKeys = Object.keys(value); + for ( var i = 0, leni = itemKeys.length; i < leni; ++i ) + { + var itemElements = {}; + var subSize = 0; + itemData = value[itemKeys[i]]; + + // check data + if ( itemData === null || itemData === 0 ) { + continue; + } + + // elements + var subElement; + var elemKeys = Object.keys(itemData); + for ( var j = 0, lenj = elemKeys.length; j < lenj; ++j ) + { + subElement = dwv.dicom.getDicomElement(elemKeys[j]); + subSize += dwv.dicom.setElementValue(subElement, itemData[elemKeys[j]]); + + // add sequence delimitation size for sub sequences + if (dwv.dicom.isImplicitLengthSequence(subElement)) { + subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit); + } + + name = dwv.dicom.getGroupElementKey(subElement.tag.group, subElement.tag.element); + itemElements[name] = subElement; + subSize += dwv.dicom.getDataElementPrefixByteSize(subElement.vr, isImplicit); + } + + // item (after elements to get the size) + var itemElement = { + 'tag': { 'group': "0xFFFE", 'element': "0xE000" }, + 'vr': "NONE", + 'vl': (explicitLength ? subSize : "u/l"), + 'value': [] + }; + name = dwv.dicom.getGroupElementKey(itemElement.tag.group, itemElement.tag.element); + itemElements[name] = itemElement; + subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit); + + // item delimitation + if (!explicitLength) { + var itemDelimElement = { + 'tag': { 'group': "0xFFFE", 'element': "0xE00D" }, + 'vr': "NONE", + 'vl': 0, + 'value': [] + }; + name = dwv.dicom.getGroupElementKey(itemDelimElement.tag.group, itemDelimElement.tag.element); + itemElements[name] = itemDelimElement; + subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit); + } + + size += subSize; + sqItems.push(itemElements); + } + + element.value = sqItems; + if (explicitLength) { + element.vl = size; + } else { + element.vl = "u/l"; + } + } + } + else { + // set the value and calculate size + size = 0; + if (value instanceof Array) { + element.value = value; + for (var k = 0; k < value.length; ++k) { + // spearator + if (k !== 0) { + size += 1; + } + // value + size += value[k].toString().length; + } + } + else { + element.value = [value]; + if (typeof value !== "undefined" && typeof value.length !== "undefined") { + size = value.length; + } + else { + // numbers + size = 1; + } + } + + // convert size to bytes + if ( element.vr === "US" || element.vr === "OW") { + size *= Uint16Array.BYTES_PER_ELEMENT; + } + else if ( element.vr === "SS") { + size *= Int16Array.BYTES_PER_ELEMENT; + } + else if ( element.vr === "UL") { + size *= Uint32Array.BYTES_PER_ELEMENT; + } + else if ( element.vr === "SL") { + size *= Int32Array.BYTES_PER_ELEMENT; + } + else if ( element.vr === "FL") { + size *= Float32Array.BYTES_PER_ELEMENT; + } + else if ( element.vr === "FD") { + size *= Float64Array.BYTES_PER_ELEMENT; + } + else { + size *= Uint8Array.BYTES_PER_ELEMENT; + } + element.vl = size; + } + + // return the size of that data + return size; +}; + +/** + * Get the DICOM element from a DICOM tags object. + * @param {Object} tags The DICOM tags object. + * @return {Object} The DICOM elements and the end offset. + */ +dwv.dicom.getElementsFromJSONTags = function (tags) { + // transfer syntax + var isImplicit = dwv.dicom.isImplicitTransferSyntax(tags.TransferSyntaxUID); + // convert JSON to DICOM element object + var keys = Object.keys(tags); + var dicomElements = {}; + var dicomElement; + var name; + var offset = 128 + 4; // preamble + var size; + for ( var k = 0, len = keys.length; k < len; ++k ) + { + // get the DICOM element definition from its name + dicomElement = dwv.dicom.getDicomElement(keys[k]); + // set its value + size = dwv.dicom.setElementValue(dicomElement, tags[keys[k]], isImplicit); + // set offsets + offset += dwv.dicom.getDataElementPrefixByteSize(dicomElement.vr, isImplicit); + dicomElement.startOffset = offset; + offset += size; + dicomElement.endOffset = offset; + // create the tag group/element key + name = dwv.dicom.getGroupElementKey(dicomElement.tag.group, dicomElement.tag.element); + // store + dicomElements[name] = dicomElement; + } + // return + return {'elements': dicomElements, 'offset': offset }; +}; + +/** + * GradSquarePixGenerator + * Generates a small gradient square. + * @param {Number} numberOfColumns The image number of columns. + * @param {Number} numberOfRows The image number of rows. + * @constructor + */ +var GradSquarePixGenerator = function (numberOfColumns, numberOfRows) { + + var halfCols = numberOfColumns * 0.5; + var halfRows = numberOfRows * 0.5; + var maxNoBounds = (numberOfColumns/2 + halfCols/2) * (numberOfRows/2 + halfRows/2); + var max = 100; + + /** + * Get a grey value. + * @param {Number} i The column index. + * @param {Number} j The row index. + * @return {Array} The grey value. + */ + this.getGrey = function (i,j) { + var value = max; + var jc = Math.abs( j - halfRows ); + var ic = Math.abs( i - halfCols ); + if ( jc < halfRows/2 && ic < halfCols/2 ) { + value += (i * j) * (max / maxNoBounds); + } + return [value]; + }; + + /** + * Get RGB values. + * @param {Number} i The column index. + * @param {Number} j The row index. + * @return {Array} The [R,G,B] values. + */ + this.getRGB = function (i,j) { + var value = 0; + var jc = Math.abs( j - halfRows ); + var ic = Math.abs( i - halfCols ); + if ( jc < halfRows/2 && ic < halfCols/2 ) { + value += (i * j) * (max / maxNoBounds); + } + if (value > 255 ) { + value = 200; + } + return [0, value, value]; + }; +}; + +// List of pixel generators. +dwv.dicom.pixelGenerators = { + 'gradSquare': GradSquarePixGenerator +}; + +/** + * Get the DICOM pixel data from a DICOM tags object. + * @param {Object} tags The DICOM tags object. + * @param {Object} startOffset The start offset of the pixel data. + * @param {String} pixGeneratorName The name of a pixel generator. + * @return {Object} The DICOM pixel data element. + */ +dwv.dicom.generatePixelDataFromJSONTags = function (tags, startOffset, pixGeneratorName) { + + // default generator + if ( typeof pixGeneratorName === "undefined" ) { + pixGeneratorName = "gradSquare"; + } + + // check tags + if ( typeof tags.TransferSyntaxUID === "undefined" ) { + throw new Error("Missing transfer syntax for pixel generation."); + } else if ( typeof tags.Rows === "undefined" ) { + throw new Error("Missing number of rows for pixel generation."); + } else if ( typeof tags.Columns === "undefined" ) { + throw new Error("Missing number of columns for pixel generation."); + } else if ( typeof tags.BitsAllocated === "undefined" ) { + throw new Error("Missing BitsAllocated for pixel generation."); + } else if ( typeof tags.PixelRepresentation === "undefined" ) { + throw new Error("Missing PixelRepresentation for pixel generation."); + } else if ( typeof tags.SamplesPerPixel === "undefined" ) { + throw new Error("Missing SamplesPerPixel for pixel generation."); + } else if ( typeof tags.PhotometricInterpretation === "undefined" ) { + throw new Error("Missing PhotometricInterpretation for pixel generation."); + } + + // extract info from tags + var isImplicit = dwv.dicom.isImplicitTransferSyntax(tags.TransferSyntaxUID); + var numberOfRows = tags.Rows; + var numberOfColumns = tags.Columns; + var bitsAllocated = tags.BitsAllocated; + var pixelRepresentation = tags.PixelRepresentation; + var samplesPerPixel = tags.SamplesPerPixel; + var photometricInterpretation = tags.PhotometricInterpretation; + + var sliceLength = numberOfRows * numberOfColumns; + var dataLength = sliceLength * samplesPerPixel; + + // check values + if (samplesPerPixel !== 1 && samplesPerPixel !== 3) { + throw new Error("Unsupported SamplesPerPixel for pixel generation: "+samplesPerPixel); + } + if ( (samplesPerPixel === 1 && !( photometricInterpretation === "MONOCHROME1" || + photometricInterpretation === "MONOCHROME2" ) ) || + ( samplesPerPixel === 3 && photometricInterpretation !== "RGB" ) ) { + throw new Error("Unsupported PhotometricInterpretation for pixel generation: " + + photometricInterpretation + " with SamplesPerPixel: " + samplesPerPixel); + } + + var nSamples = 1; + var nColourPlanes = 1; + if ( samplesPerPixel === 3 ) { + if ( typeof tags.PlanarConfiguration === "undefined" ) { + throw new Error("Missing PlanarConfiguration for pixel generation."); + } + var planarConfiguration = tags.PlanarConfiguration; + if ( planarConfiguration !== 0 && planarConfiguration !== 1 ) { + throw new Error("Unsupported PlanarConfiguration for pixel generation: "+planarConfiguration); + } + if ( planarConfiguration === 0 ) { + nSamples = 3; + } else { + nColourPlanes = 3; + } + } + + // create pixel array + var pixels = dwv.dicom.getTypedArray( + bitsAllocated, pixelRepresentation, dataLength ); + + // pixels generator + if (typeof dwv.dicom.pixelGenerators[pixGeneratorName] === "undefined" ) { + throw new Error("Unknown PixelData generator: "+pixGeneratorName); + } + var generator = new dwv.dicom.pixelGenerators[pixGeneratorName](numberOfColumns, numberOfRows); + var generate = generator.getGrey; + if (photometricInterpretation === "RGB") { + generate = generator.getRGB; + } + + // main loop + var offset = 0; + for ( var c = 0; c < nColourPlanes; ++c ) { + for ( var j = 0; j < numberOfRows; ++j ) { + for ( var i = 0; i < numberOfColumns; ++i ) { + for ( var s = 0; s < nSamples; ++s ) { + if ( nColourPlanes !== 1 ) { + pixels[offset] = generate(i,j)[c]; + } else { + pixels[offset] = generate(i,j)[s]; + } + ++offset; + } + } + } + } + + // create and return the DICOM element + var vr = "OW"; + if ( bitsAllocated === 8 ) { + vr = "OB"; + } + var pixVL = dwv.dicom.getDataElementPrefixByteSize(vr, isImplicit) + + (pixels.BYTES_PER_ELEMENT * dataLength); + return { + 'tag': { 'group': "0x7FE0", 'element': "0x0010" }, + 'vr': vr, + 'vl': pixVL, + 'value': pixels, + 'startOffset': startOffset, + 'endOffset': startOffset + pixVL + }; +}; + +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + +/** + * DICOM tag dictionary. + * Generated using xml standard conversion + * from {@link https://github.com/ivmartel/dcmbench/tree/master/view/part06} + * with {@link http://medical.nema.org/medical/dicom/current/source/docbook/part06/part06.xml} + * Conversion changes: + * - (vr) "See Note" -> "NONE", "OB or OW" -> "ox", "US or SS" -> "xs" + * - added "GenericGroupLength" element to each group + * Local changes: + * - tag numbers with 'xx' were replaced with '00', 'xxx' with '001' and 'xxxx' with '0004' + */ +dwv.dicom.dictionary = { + '0x0000': { + '0x0000': ['UL', '1', 'GroupLength'], + '0x0001': ['UL', '1', 'CommandLengthToEnd'], + '0x0002': ['UI', '1', 'AffectedSOPClassUID'], + '0x0003': ['UI', '1', 'RequestedSOPClassUID'], + '0x0010': ['CS', '1', 'CommandRecognitionCode'], + '0x0100': ['US', '1', 'CommandField'], + '0x0110': ['US', '1', 'MessageID'], + '0x0120': ['US', '1', 'MessageIDBeingRespondedTo'], + '0x0200': ['AE', '1', 'Initiator'], + '0x0300': ['AE', '1', 'Receiver'], + '0x0400': ['AE', '1', 'FindLocation'], + '0x0600': ['AE', '1', 'MoveDestination'], + '0x0700': ['US', '1', 'Priority'], + '0x0800': ['US', '1', 'DataSetType'], + '0x0850': ['US', '1', 'NumberOfMatches'], + '0x0860': ['US', '1', 'ResponseSequenceNumber'], + '0x0900': ['US', '1', 'Status'], + '0x0901': ['AT', '1-n', 'OffendingElement'], + '0x0902': ['LO', '1', 'ErrorComment'], + '0x0903': ['US', '1', 'ErrorID'], + '0x0904': ['OT', '1-n', 'ErrorInformation'], + '0x1000': ['UI', '1', 'AffectedSOPInstanceUID'], + '0x1001': ['UI', '1', 'RequestedSOPInstanceUID'], + '0x1002': ['US', '1', 'EventTypeID'], + '0x1003': ['OT', '1-n', 'EventInformation'], + '0x1005': ['AT', '1-n', 'AttributeIdentifierList'], + '0x1007': ['AT', '1-n', 'ModificationList'], + '0x1008': ['US', '1', 'ActionTypeID'], + '0x1009': ['OT', '1-n', 'ActionInformation'], + '0x1013': ['UI', '1-n', 'SuccessfulSOPInstanceUIDList'], + '0x1014': ['UI', '1-n', 'FailedSOPInstanceUIDList'], + '0x1015': ['UI', '1-n', 'WarningSOPInstanceUIDList'], + '0x1020': ['US', '1', 'NumberOfRemainingSuboperations'], + '0x1021': ['US', '1', 'NumberOfCompletedSuboperations'], + '0x1022': ['US', '1', 'NumberOfFailedSuboperations'], + '0x1023': ['US', '1', 'NumberOfWarningSuboperations'], + '0x1030': ['AE', '1', 'MoveOriginatorApplicationEntityTitle'], + '0x1031': ['US', '1', 'MoveOriginatorMessageID'], + '0x4000': ['AT', '1', 'DialogReceiver'], + '0x4010': ['AT', '1', 'TerminalType'], + '0x5010': ['SH', '1', 'MessageSetID'], + '0x5020': ['SH', '1', 'EndMessageSet'], + '0x5110': ['AT', '1', 'DisplayFormat'], + '0x5120': ['AT', '1', 'PagePositionID'], + '0x5130': ['CS', '1', 'TextFormatID'], + '0x5140': ['CS', '1', 'NormalReverse'], + '0x5150': ['CS', '1', 'AddGrayScale'], + '0x5160': ['CS', '1', 'Borders'], + '0x5170': ['IS', '1', 'Copies'], + '0x5180': ['CS', '1', 'OldMagnificationType'], + '0x5190': ['CS', '1', 'Erase'], + '0x51A0': ['CS', '1', 'Print'], + '0x51B0': ['US', '1-n', 'Overlays'] + }, + '0x0002': { + '0x0000': ['UL', '1', 'FileMetaInformationGroupLength'], + '0x0001': ['OB', '1', 'FileMetaInformationVersion'], + '0x0002': ['UI', '1', 'MediaStorageSOPClassUID'], + '0x0003': ['UI', '1', 'MediaStorageSOPInstanceUID'], + '0x0010': ['UI', '1', 'TransferSyntaxUID'], + '0x0012': ['UI', '1', 'ImplementationClassUID'], + '0x0013': ['SH', '1', 'ImplementationVersionName'], + '0x0016': ['AE', '1', 'SourceApplicationEntityTitle'], + '0x0017': ['AE', '1', 'SendingApplicationEntityTitle'], + '0x0018': ['AE', '1', 'ReceivingApplicationEntityTitle'], + '0x0100': ['UI', '1', 'PrivateInformationCreatorUID'], + '0x0102': ['OB', '1', 'PrivateInformation'] + }, + '0x0004': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x1130': ['CS', '1', 'FileSetID'], + '0x1141': ['CS', '1-8', 'FileSetDescriptorFileID'], + '0x1142': ['CS', '1', 'SpecificCharacterSetOfFileSetDescriptorFile'], + '0x1200': ['UL', '1', 'OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntity'], + '0x1202': ['UL', '1', 'OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity'], + '0x1212': ['US', '1', 'FileSetConsistencyFlag'], + '0x1220': ['SQ', '1', 'DirectoryRecordSequence'], + '0x1400': ['UL', '1', 'OffsetOfTheNextDirectoryRecord'], + '0x1410': ['US', '1', 'RecordInUseFlag'], + '0x1420': ['UL', '1', 'OffsetOfReferencedLowerLevelDirectoryEntity'], + '0x1430': ['CS', '1', 'DirectoryRecordType'], + '0x1432': ['UI', '1', 'PrivateRecordUID'], + '0x1500': ['CS', '1-8', 'ReferencedFileID'], + '0x1504': ['UL', '1', 'MRDRDirectoryRecordOffset'], + '0x1510': ['UI', '1', 'ReferencedSOPClassUIDInFile'], + '0x1511': ['UI', '1', 'ReferencedSOPInstanceUIDInFile'], + '0x1512': ['UI', '1', 'ReferencedTransferSyntaxUIDInFile'], + '0x151A': ['UI', '1-n', 'ReferencedRelatedGeneralSOPClassUIDInFile'], + '0x1600': ['UL', '1', 'NumberOfReferences'] + }, + '0x0008': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['UL', '1', 'LengthToEnd'], + '0x0005': ['CS', '1-n', 'SpecificCharacterSet'], + '0x0006': ['SQ', '1', 'LanguageCodeSequence'], + '0x0008': ['CS', '2-n', 'ImageType'], + '0x0010': ['SH', '1', 'RecognitionCode'], + '0x0012': ['DA', '1', 'InstanceCreationDate'], + '0x0013': ['TM', '1', 'InstanceCreationTime'], + '0x0014': ['UI', '1', 'InstanceCreatorUID'], + '0x0015': ['DT', '1', 'InstanceCoercionDateTime'], + '0x0016': ['UI', '1', 'SOPClassUID'], + '0x0018': ['UI', '1', 'SOPInstanceUID'], + '0x001A': ['UI', '1-n', 'RelatedGeneralSOPClassUID'], + '0x001B': ['UI', '1', 'OriginalSpecializedSOPClassUID'], + '0x0020': ['DA', '1', 'StudyDate'], + '0x0021': ['DA', '1', 'SeriesDate'], + '0x0022': ['DA', '1', 'AcquisitionDate'], + '0x0023': ['DA', '1', 'ContentDate'], + '0x0024': ['DA', '1', 'OverlayDate'], + '0x0025': ['DA', '1', 'CurveDate'], + '0x002A': ['DT', '1', 'AcquisitionDateTime'], + '0x0030': ['TM', '1', 'StudyTime'], + '0x0031': ['TM', '1', 'SeriesTime'], + '0x0032': ['TM', '1', 'AcquisitionTime'], + '0x0033': ['TM', '1', 'ContentTime'], + '0x0034': ['TM', '1', 'OverlayTime'], + '0x0035': ['TM', '1', 'CurveTime'], + '0x0040': ['US', '1', 'DataSetType'], + '0x0041': ['LO', '1', 'DataSetSubtype'], + '0x0042': ['CS', '1', 'NuclearMedicineSeriesType'], + '0x0050': ['SH', '1', 'AccessionNumber'], + '0x0051': ['SQ', '1', 'IssuerOfAccessionNumberSequence'], + '0x0052': ['CS', '1', 'QueryRetrieveLevel'], + '0x0053': ['CS', '1', 'QueryRetrieveView'], + '0x0054': ['AE', '1-n', 'RetrieveAETitle'], + '0x0056': ['CS', '1', 'InstanceAvailability'], + '0x0058': ['UI', '1-n', 'FailedSOPInstanceUIDList'], + '0x0060': ['CS', '1', 'Modality'], + '0x0061': ['CS', '1-n', 'ModalitiesInStudy'], + '0x0062': ['UI', '1-n', 'SOPClassesInStudy'], + '0x0064': ['CS', '1', 'ConversionType'], + '0x0068': ['CS', '1', 'PresentationIntentType'], + '0x0070': ['LO', '1', 'Manufacturer'], + '0x0080': ['LO', '1', 'InstitutionName'], + '0x0081': ['ST', '1', 'InstitutionAddress'], + '0x0082': ['SQ', '1', 'InstitutionCodeSequence'], + '0x0090': ['PN', '1', 'ReferringPhysicianName'], + '0x0092': ['ST', '1', 'ReferringPhysicianAddress'], + '0x0094': ['SH', '1-n', 'ReferringPhysicianTelephoneNumbers'], + '0x0096': ['SQ', '1', 'ReferringPhysicianIdentificationSequence'], + '0x009C': ['PN', '1-n', 'ConsultingPhysicianName'], + '0x009D': ['SQ', '1', 'ConsultingPhysicianIdentificationSequence'], + '0x0100': ['SH', '1', 'CodeValue'], + '0x0101': ['LO', '1', 'ExtendedCodeValue'], + '0x0102': ['SH', '1', 'CodingSchemeDesignator'], + '0x0103': ['SH', '1', 'CodingSchemeVersion'], + '0x0104': ['LO', '1', 'CodeMeaning'], + '0x0105': ['CS', '1', 'MappingResource'], + '0x0106': ['DT', '1', 'ContextGroupVersion'], + '0x0107': ['DT', '1', 'ContextGroupLocalVersion'], + '0x0108': ['LT', '1', 'ExtendedCodeMeaning'], + '0x010B': ['CS', '1', 'ContextGroupExtensionFlag'], + '0x010C': ['UI', '1', 'CodingSchemeUID'], + '0x010D': ['UI', '1', 'ContextGroupExtensionCreatorUID'], + '0x010F': ['CS', '1', 'ContextIdentifier'], + '0x0110': ['SQ', '1', 'CodingSchemeIdentificationSequence'], + '0x0112': ['LO', '1', 'CodingSchemeRegistry'], + '0x0114': ['ST', '1', 'CodingSchemeExternalID'], + '0x0115': ['ST', '1', 'CodingSchemeName'], + '0x0116': ['ST', '1', 'CodingSchemeResponsibleOrganization'], + '0x0117': ['UI', '1', 'ContextUID'], + '0x0118': ['UI', '1', 'MappingResourceUID'], + '0x0119': ['UC', '1', 'LongCodeValue'], + '0x0120': ['UR', '1', 'URNCodeValue'], + '0x0121': ['SQ', '1', 'EquivalentCodeSequence'], + '0x0201': ['SH', '1', 'TimezoneOffsetFromUTC'], + '0x0300': ['SQ', '1', 'PrivateDataElementCharacteristicsSequence'], + '0x0301': ['US', '1', 'PrivateGroupReference'], + '0x0302': ['LO', '1', 'PrivateCreatorReference'], + '0x0303': ['CS', '1', 'BlockIdentifyingInformationStatus'], + '0x0304': ['US', '1-n', 'NonidentifyingPrivateElements'], + '0x0306': ['US', '1-n', 'IdentifyingPrivateElements'], + '0x0305': ['SQ', '1', 'DeidentificationActionSequence'], + '0x0307': ['CS', '1', 'DeidentificationAction'], + '0x1000': ['AE', '1', 'NetworkID'], + '0x1010': ['SH', '1', 'StationName'], + '0x1030': ['LO', '1', 'StudyDescription'], + '0x1032': ['SQ', '1', 'ProcedureCodeSequence'], + '0x103E': ['LO', '1', 'SeriesDescription'], + '0x103F': ['SQ', '1', 'SeriesDescriptionCodeSequence'], + '0x1040': ['LO', '1', 'InstitutionalDepartmentName'], + '0x1048': ['PN', '1-n', 'PhysiciansOfRecord'], + '0x1049': ['SQ', '1', 'PhysiciansOfRecordIdentificationSequence'], + '0x1050': ['PN', '1-n', 'PerformingPhysicianName'], + '0x1052': ['SQ', '1', 'PerformingPhysicianIdentificationSequence'], + '0x1060': ['PN', '1-n', 'NameOfPhysiciansReadingStudy'], + '0x1062': ['SQ', '1', 'PhysiciansReadingStudyIdentificationSequence'], + '0x1070': ['PN', '1-n', 'OperatorsName'], + '0x1072': ['SQ', '1', 'OperatorIdentificationSequence'], + '0x1080': ['LO', '1-n', 'AdmittingDiagnosesDescription'], + '0x1084': ['SQ', '1', 'AdmittingDiagnosesCodeSequence'], + '0x1090': ['LO', '1', 'ManufacturerModelName'], + '0x1100': ['SQ', '1', 'ReferencedResultsSequence'], + '0x1110': ['SQ', '1', 'ReferencedStudySequence'], + '0x1111': ['SQ', '1', 'ReferencedPerformedProcedureStepSequence'], + '0x1115': ['SQ', '1', 'ReferencedSeriesSequence'], + '0x1120': ['SQ', '1', 'ReferencedPatientSequence'], + '0x1125': ['SQ', '1', 'ReferencedVisitSequence'], + '0x1130': ['SQ', '1', 'ReferencedOverlaySequence'], + '0x1134': ['SQ', '1', 'ReferencedStereometricInstanceSequence'], + '0x113A': ['SQ', '1', 'ReferencedWaveformSequence'], + '0x1140': ['SQ', '1', 'ReferencedImageSequence'], + '0x1145': ['SQ', '1', 'ReferencedCurveSequence'], + '0x114A': ['SQ', '1', 'ReferencedInstanceSequence'], + '0x114B': ['SQ', '1', 'ReferencedRealWorldValueMappingInstanceSequence'], + '0x1150': ['UI', '1', 'ReferencedSOPClassUID'], + '0x1155': ['UI', '1', 'ReferencedSOPInstanceUID'], + '0x115A': ['UI', '1-n', 'SOPClassesSupported'], + '0x1160': ['IS', '1-n', 'ReferencedFrameNumber'], + '0x1161': ['UL', '1-n', 'SimpleFrameList'], + '0x1162': ['UL', '3-3n', 'CalculatedFrameList'], + '0x1163': ['FD', '2', 'TimeRange'], + '0x1164': ['SQ', '1', 'FrameExtractionSequence'], + '0x1167': ['UI', '1', 'MultiFrameSourceSOPInstanceUID'], + '0x1190': ['UR', '1', 'RetrieveURL'], + '0x1195': ['UI', '1', 'TransactionUID'], + '0x1196': ['US', '1', 'WarningReason'], + '0x1197': ['US', '1', 'FailureReason'], + '0x1198': ['SQ', '1', 'FailedSOPSequence'], + '0x1199': ['SQ', '1', 'ReferencedSOPSequence'], + '0x1200': ['SQ', '1', 'StudiesContainingOtherReferencedInstancesSequence'], + '0x1250': ['SQ', '1', 'RelatedSeriesSequence'], + '0x2110': ['CS', '1', 'LossyImageCompressionRetired'], + '0x2111': ['ST', '1', 'DerivationDescription'], + '0x2112': ['SQ', '1', 'SourceImageSequence'], + '0x2120': ['SH', '1', 'StageName'], + '0x2122': ['IS', '1', 'StageNumber'], + '0x2124': ['IS', '1', 'NumberOfStages'], + '0x2127': ['SH', '1', 'ViewName'], + '0x2128': ['IS', '1', 'ViewNumber'], + '0x2129': ['IS', '1', 'NumberOfEventTimers'], + '0x212A': ['IS', '1', 'NumberOfViewsInStage'], + '0x2130': ['DS', '1-n', 'EventElapsedTimes'], + '0x2132': ['LO', '1-n', 'EventTimerNames'], + '0x2133': ['SQ', '1', 'EventTimerSequence'], + '0x2134': ['FD', '1', 'EventTimeOffset'], + '0x2135': ['SQ', '1', 'EventCodeSequence'], + '0x2142': ['IS', '1', 'StartTrim'], + '0x2143': ['IS', '1', 'StopTrim'], + '0x2144': ['IS', '1', 'RecommendedDisplayFrameRate'], + '0x2200': ['CS', '1', 'TransducerPosition'], + '0x2204': ['CS', '1', 'TransducerOrientation'], + '0x2208': ['CS', '1', 'AnatomicStructure'], + '0x2218': ['SQ', '1', 'AnatomicRegionSequence'], + '0x2220': ['SQ', '1', 'AnatomicRegionModifierSequence'], + '0x2228': ['SQ', '1', 'PrimaryAnatomicStructureSequence'], + '0x2229': ['SQ', '1', 'AnatomicStructureSpaceOrRegionSequence'], + '0x2230': ['SQ', '1', 'PrimaryAnatomicStructureModifierSequence'], + '0x2240': ['SQ', '1', 'TransducerPositionSequence'], + '0x2242': ['SQ', '1', 'TransducerPositionModifierSequence'], + '0x2244': ['SQ', '1', 'TransducerOrientationSequence'], + '0x2246': ['SQ', '1', 'TransducerOrientationModifierSequence'], + '0x2251': ['SQ', '1', 'AnatomicStructureSpaceOrRegionCodeSequenceTrial'], + '0x2253': ['SQ', '1', 'AnatomicPortalOfEntranceCodeSequenceTrial'], + '0x2255': ['SQ', '1', 'AnatomicApproachDirectionCodeSequenceTrial'], + '0x2256': ['ST', '1', 'AnatomicPerspectiveDescriptionTrial'], + '0x2257': ['SQ', '1', 'AnatomicPerspectiveCodeSequenceTrial'], + '0x2258': ['ST', '1', 'AnatomicLocationOfExaminingInstrumentDescriptionTrial'], + '0x2259': ['SQ', '1', 'AnatomicLocationOfExaminingInstrumentCodeSequenceTrial'], + '0x225A': ['SQ', '1', 'AnatomicStructureSpaceOrRegionModifierCodeSequenceTrial'], + '0x225C': ['SQ', '1', 'OnAxisBackgroundAnatomicStructureCodeSequenceTrial'], + '0x3001': ['SQ', '1', 'AlternateRepresentationSequence'], + '0x3010': ['UI', '1-n', 'IrradiationEventUID'], + '0x3011': ['SQ', '1', 'SourceIrradiationEventSequence'], + '0x3012': ['UI', '1', 'RadiopharmaceuticalAdministrationEventUID'], + '0x4000': ['LT', '1', 'IdentifyingComments'], + '0x9007': ['CS', '4', 'FrameType'], + '0x9092': ['SQ', '1', 'ReferencedImageEvidenceSequence'], + '0x9121': ['SQ', '1', 'ReferencedRawDataSequence'], + '0x9123': ['UI', '1', 'CreatorVersionUID'], + '0x9124': ['SQ', '1', 'DerivationImageSequence'], + '0x9154': ['SQ', '1', 'SourceImageEvidenceSequence'], + '0x9205': ['CS', '1', 'PixelPresentation'], + '0x9206': ['CS', '1', 'VolumetricProperties'], + '0x9207': ['CS', '1', 'VolumeBasedCalculationTechnique'], + '0x9208': ['CS', '1', 'ComplexImageComponent'], + '0x9209': ['CS', '1', 'AcquisitionContrast'], + '0x9215': ['SQ', '1', 'DerivationCodeSequence'], + '0x9237': ['SQ', '1', 'ReferencedPresentationStateSequence'], + '0x9410': ['SQ', '1', 'ReferencedOtherPlaneSequence'], + '0x9458': ['SQ', '1', 'FrameDisplaySequence'], + '0x9459': ['FL', '1', 'RecommendedDisplayFrameRateInFloat'], + '0x9460': ['CS', '1', 'SkipFrameRangeFlag'] + }, + '0x0010': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['PN', '1', 'PatientName'], + '0x0020': ['LO', '1', 'PatientID'], + '0x0021': ['LO', '1', 'IssuerOfPatientID'], + '0x0022': ['CS', '1', 'TypeOfPatientID'], + '0x0024': ['SQ', '1', 'IssuerOfPatientIDQualifiersSequence'], + '0x0030': ['DA', '1', 'PatientBirthDate'], + '0x0032': ['TM', '1', 'PatientBirthTime'], + '0x0040': ['CS', '1', 'PatientSex'], + '0x0050': ['SQ', '1', 'PatientInsurancePlanCodeSequence'], + '0x0101': ['SQ', '1', 'PatientPrimaryLanguageCodeSequence'], + '0x0102': ['SQ', '1', 'PatientPrimaryLanguageModifierCodeSequence'], + '0x0200': ['CS', '1', 'QualityControlSubject'], + '0x0201': ['SQ', '1', 'QualityControlSubjectTypeCodeSequence'], + '0x1000': ['LO', '1-n', 'OtherPatientIDs'], + '0x1001': ['PN', '1-n', 'OtherPatientNames'], + '0x1002': ['SQ', '1', 'OtherPatientIDsSequence'], + '0x1005': ['PN', '1', 'PatientBirthName'], + '0x1010': ['AS', '1', 'PatientAge'], + '0x1020': ['DS', '1', 'PatientSize'], + '0x1021': ['SQ', '1', 'PatientSizeCodeSequence'], + '0x1030': ['DS', '1', 'PatientWeight'], + '0x1040': ['LO', '1', 'PatientAddress'], + '0x1050': ['LO', '1-n', 'InsurancePlanIdentification'], + '0x1060': ['PN', '1', 'PatientMotherBirthName'], + '0x1080': ['LO', '1', 'MilitaryRank'], + '0x1081': ['LO', '1', 'BranchOfService'], + '0x1090': ['LO', '1', 'MedicalRecordLocator'], + '0x1100': ['SQ', '1', 'ReferencedPatientPhotoSequence'], + '0x2000': ['LO', '1-n', 'MedicalAlerts'], + '0x2110': ['LO', '1-n', 'Allergies'], + '0x2150': ['LO', '1', 'CountryOfResidence'], + '0x2152': ['LO', '1', 'RegionOfResidence'], + '0x2154': ['SH', '1-n', 'PatientTelephoneNumbers'], + '0x2155': ['LT', '1', 'PatientTelecomInformation'], + '0x2160': ['SH', '1', 'EthnicGroup'], + '0x2180': ['SH', '1', 'Occupation'], + '0x21A0': ['CS', '1', 'SmokingStatus'], + '0x21B0': ['LT', '1', 'AdditionalPatientHistory'], + '0x21C0': ['US', '1', 'PregnancyStatus'], + '0x21D0': ['DA', '1', 'LastMenstrualDate'], + '0x21F0': ['LO', '1', 'PatientReligiousPreference'], + '0x2201': ['LO', '1', 'PatientSpeciesDescription'], + '0x2202': ['SQ', '1', 'PatientSpeciesCodeSequence'], + '0x2203': ['CS', '1', 'PatientSexNeutered'], + '0x2210': ['CS', '1', 'AnatomicalOrientationType'], + '0x2292': ['LO', '1', 'PatientBreedDescription'], + '0x2293': ['SQ', '1', 'PatientBreedCodeSequence'], + '0x2294': ['SQ', '1', 'BreedRegistrationSequence'], + '0x2295': ['LO', '1', 'BreedRegistrationNumber'], + '0x2296': ['SQ', '1', 'BreedRegistryCodeSequence'], + '0x2297': ['PN', '1', 'ResponsiblePerson'], + '0x2298': ['CS', '1', 'ResponsiblePersonRole'], + '0x2299': ['LO', '1', 'ResponsibleOrganization'], + '0x4000': ['LT', '1', 'PatientComments'], + '0x9431': ['FL', '1', 'ExaminedBodyThickness'] + }, + '0x0012': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['LO', '1', 'ClinicalTrialSponsorName'], + '0x0020': ['LO', '1', 'ClinicalTrialProtocolID'], + '0x0021': ['LO', '1', 'ClinicalTrialProtocolName'], + '0x0030': ['LO', '1', 'ClinicalTrialSiteID'], + '0x0031': ['LO', '1', 'ClinicalTrialSiteName'], + '0x0040': ['LO', '1', 'ClinicalTrialSubjectID'], + '0x0042': ['LO', '1', 'ClinicalTrialSubjectReadingID'], + '0x0050': ['LO', '1', 'ClinicalTrialTimePointID'], + '0x0051': ['ST', '1', 'ClinicalTrialTimePointDescription'], + '0x0060': ['LO', '1', 'ClinicalTrialCoordinatingCenterName'], + '0x0062': ['CS', '1', 'PatientIdentityRemoved'], + '0x0063': ['LO', '1-n', 'DeidentificationMethod'], + '0x0064': ['SQ', '1', 'DeidentificationMethodCodeSequence'], + '0x0071': ['LO', '1', 'ClinicalTrialSeriesID'], + '0x0072': ['LO', '1', 'ClinicalTrialSeriesDescription'], + '0x0081': ['LO', '1', 'ClinicalTrialProtocolEthicsCommitteeName'], + '0x0082': ['LO', '1', 'ClinicalTrialProtocolEthicsCommitteeApprovalNumber'], + '0x0083': ['SQ', '1', 'ConsentForClinicalTrialUseSequence'], + '0x0084': ['CS', '1', 'DistributionType'], + '0x0085': ['CS', '1', 'ConsentForDistributionFlag'] + }, + '0x0014': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0023': ['ST', '1-n', 'CADFileFormat'], + '0x0024': ['ST', '1-n', 'ComponentReferenceSystem'], + '0x0025': ['ST', '1-n', 'ComponentManufacturingProcedure'], + '0x0028': ['ST', '1-n', 'ComponentManufacturer'], + '0x0030': ['DS', '1-n', 'MaterialThickness'], + '0x0032': ['DS', '1-n', 'MaterialPipeDiameter'], + '0x0034': ['DS', '1-n', 'MaterialIsolationDiameter'], + '0x0042': ['ST', '1-n', 'MaterialGrade'], + '0x0044': ['ST', '1-n', 'MaterialPropertiesDescription'], + '0x0045': ['ST', '1-n', 'MaterialPropertiesFileFormatRetired'], + '0x0046': ['LT', '1', 'MaterialNotes'], + '0x0050': ['CS', '1', 'ComponentShape'], + '0x0052': ['CS', '1', 'CurvatureType'], + '0x0054': ['DS', '1', 'OuterDiameter'], + '0x0056': ['DS', '1', 'InnerDiameter'], + '0x1010': ['ST', '1', 'ActualEnvironmentalConditions'], + '0x1020': ['DA', '1', 'ExpiryDate'], + '0x1040': ['ST', '1', 'EnvironmentalConditions'], + '0x2002': ['SQ', '1', 'EvaluatorSequence'], + '0x2004': ['IS', '1', 'EvaluatorNumber'], + '0x2006': ['PN', '1', 'EvaluatorName'], + '0x2008': ['IS', '1', 'EvaluationAttempt'], + '0x2012': ['SQ', '1', 'IndicationSequence'], + '0x2014': ['IS', '1', 'IndicationNumber'], + '0x2016': ['SH', '1', 'IndicationLabel'], + '0x2018': ['ST', '1', 'IndicationDescription'], + '0x201A': ['CS', '1-n', 'IndicationType'], + '0x201C': ['CS', '1', 'IndicationDisposition'], + '0x201E': ['SQ', '1', 'IndicationROISequence'], + '0x2030': ['SQ', '1', 'IndicationPhysicalPropertySequence'], + '0x2032': ['SH', '1', 'PropertyLabel'], + '0x2202': ['IS', '1', 'CoordinateSystemNumberOfAxes'], + '0x2204': ['SQ', '1', 'CoordinateSystemAxesSequence'], + '0x2206': ['ST', '1', 'CoordinateSystemAxisDescription'], + '0x2208': ['CS', '1', 'CoordinateSystemDataSetMapping'], + '0x220A': ['IS', '1', 'CoordinateSystemAxisNumber'], + '0x220C': ['CS', '1', 'CoordinateSystemAxisType'], + '0x220E': ['CS', '1', 'CoordinateSystemAxisUnits'], + '0x2210': ['OB', '1', 'CoordinateSystemAxisValues'], + '0x2220': ['SQ', '1', 'CoordinateSystemTransformSequence'], + '0x2222': ['ST', '1', 'TransformDescription'], + '0x2224': ['IS', '1', 'TransformNumberOfAxes'], + '0x2226': ['IS', '1-n', 'TransformOrderOfAxes'], + '0x2228': ['CS', '1', 'TransformedAxisUnits'], + '0x222A': ['DS', '1-n', 'CoordinateSystemTransformRotationAndScaleMatrix'], + '0x222C': ['DS', '1-n', 'CoordinateSystemTransformTranslationMatrix'], + '0x3011': ['DS', '1', 'InternalDetectorFrameTime'], + '0x3012': ['DS', '1', 'NumberOfFramesIntegrated'], + '0x3020': ['SQ', '1', 'DetectorTemperatureSequence'], + '0x3022': ['ST', '1', 'SensorName'], + '0x3024': ['DS', '1', 'HorizontalOffsetOfSensor'], + '0x3026': ['DS', '1', 'VerticalOffsetOfSensor'], + '0x3028': ['DS', '1', 'SensorTemperature'], + '0x3040': ['SQ', '1', 'DarkCurrentSequence'], + '0x3050': ['ox', '1', 'DarkCurrentCounts'], + '0x3060': ['SQ', '1', 'GainCorrectionReferenceSequence'], + '0x3070': ['ox', '1', 'AirCounts'], + '0x3071': ['DS', '1', 'KVUsedInGainCalibration'], + '0x3072': ['DS', '1', 'MAUsedInGainCalibration'], + '0x3073': ['DS', '1', 'NumberOfFramesUsedForIntegration'], + '0x3074': ['LO', '1', 'FilterMaterialUsedInGainCalibration'], + '0x3075': ['DS', '1', 'FilterThicknessUsedInGainCalibration'], + '0x3076': ['DA', '1', 'DateOfGainCalibration'], + '0x3077': ['TM', '1', 'TimeOfGainCalibration'], + '0x3080': ['OB', '1', 'BadPixelImage'], + '0x3099': ['LT', '1', 'CalibrationNotes'], + '0x4002': ['SQ', '1', 'PulserEquipmentSequence'], + '0x4004': ['CS', '1', 'PulserType'], + '0x4006': ['LT', '1', 'PulserNotes'], + '0x4008': ['SQ', '1', 'ReceiverEquipmentSequence'], + '0x400A': ['CS', '1', 'AmplifierType'], + '0x400C': ['LT', '1', 'ReceiverNotes'], + '0x400E': ['SQ', '1', 'PreAmplifierEquipmentSequence'], + '0x400F': ['LT', '1', 'PreAmplifierNotes'], + '0x4010': ['SQ', '1', 'TransmitTransducerSequence'], + '0x4011': ['SQ', '1', 'ReceiveTransducerSequence'], + '0x4012': ['US', '1', 'NumberOfElements'], + '0x4013': ['CS', '1', 'ElementShape'], + '0x4014': ['DS', '1', 'ElementDimensionA'], + '0x4015': ['DS', '1', 'ElementDimensionB'], + '0x4016': ['DS', '1', 'ElementPitchA'], + '0x4017': ['DS', '1', 'MeasuredBeamDimensionA'], + '0x4018': ['DS', '1', 'MeasuredBeamDimensionB'], + '0x4019': ['DS', '1', 'LocationOfMeasuredBeamDiameter'], + '0x401A': ['DS', '1', 'NominalFrequency'], + '0x401B': ['DS', '1', 'MeasuredCenterFrequency'], + '0x401C': ['DS', '1', 'MeasuredBandwidth'], + '0x401D': ['DS', '1', 'ElementPitchB'], + '0x4020': ['SQ', '1', 'PulserSettingsSequence'], + '0x4022': ['DS', '1', 'PulseWidth'], + '0x4024': ['DS', '1', 'ExcitationFrequency'], + '0x4026': ['CS', '1', 'ModulationType'], + '0x4028': ['DS', '1', 'Damping'], + '0x4030': ['SQ', '1', 'ReceiverSettingsSequence'], + '0x4031': ['DS', '1', 'AcquiredSoundpathLength'], + '0x4032': ['CS', '1', 'AcquisitionCompressionType'], + '0x4033': ['IS', '1', 'AcquisitionSampleSize'], + '0x4034': ['DS', '1', 'RectifierSmoothing'], + '0x4035': ['SQ', '1', 'DACSequence'], + '0x4036': ['CS', '1', 'DACType'], + '0x4038': ['DS', '1-n', 'DACGainPoints'], + '0x403A': ['DS', '1-n', 'DACTimePoints'], + '0x403C': ['DS', '1-n', 'DACAmplitude'], + '0x4040': ['SQ', '1', 'PreAmplifierSettingsSequence'], + '0x4050': ['SQ', '1', 'TransmitTransducerSettingsSequence'], + '0x4051': ['SQ', '1', 'ReceiveTransducerSettingsSequence'], + '0x4052': ['DS', '1', 'IncidentAngle'], + '0x4054': ['ST', '1', 'CouplingTechnique'], + '0x4056': ['ST', '1', 'CouplingMedium'], + '0x4057': ['DS', '1', 'CouplingVelocity'], + '0x4058': ['DS', '1', 'ProbeCenterLocationX'], + '0x4059': ['DS', '1', 'ProbeCenterLocationZ'], + '0x405A': ['DS', '1', 'SoundPathLength'], + '0x405C': ['ST', '1', 'DelayLawIdentifier'], + '0x4060': ['SQ', '1', 'GateSettingsSequence'], + '0x4062': ['DS', '1', 'GateThreshold'], + '0x4064': ['DS', '1', 'VelocityOfSound'], + '0x4070': ['SQ', '1', 'CalibrationSettingsSequence'], + '0x4072': ['ST', '1', 'CalibrationProcedure'], + '0x4074': ['SH', '1', 'ProcedureVersion'], + '0x4076': ['DA', '1', 'ProcedureCreationDate'], + '0x4078': ['DA', '1', 'ProcedureExpirationDate'], + '0x407A': ['DA', '1', 'ProcedureLastModifiedDate'], + '0x407C': ['TM', '1-n', 'CalibrationTime'], + '0x407E': ['DA', '1-n', 'CalibrationDate'], + '0x4080': ['SQ', '1', 'ProbeDriveEquipmentSequence'], + '0x4081': ['CS', '1', 'DriveType'], + '0x4082': ['LT', '1', 'ProbeDriveNotes'], + '0x4083': ['SQ', '1', 'DriveProbeSequence'], + '0x4084': ['DS', '1', 'ProbeInductance'], + '0x4085': ['DS', '1', 'ProbeResistance'], + '0x4086': ['SQ', '1', 'ReceiveProbeSequence'], + '0x4087': ['SQ', '1', 'ProbeDriveSettingsSequence'], + '0x4088': ['DS', '1', 'BridgeResistors'], + '0x4089': ['DS', '1', 'ProbeOrientationAngle'], + '0x408B': ['DS', '1', 'UserSelectedGainY'], + '0x408C': ['DS', '1', 'UserSelectedPhase'], + '0x408D': ['DS', '1', 'UserSelectedOffsetX'], + '0x408E': ['DS', '1', 'UserSelectedOffsetY'], + '0x4091': ['SQ', '1', 'ChannelSettingsSequence'], + '0x4092': ['DS', '1', 'ChannelThreshold'], + '0x409A': ['SQ', '1', 'ScannerSettingsSequence'], + '0x409B': ['ST', '1', 'ScanProcedure'], + '0x409C': ['DS', '1', 'TranslationRateX'], + '0x409D': ['DS', '1', 'TranslationRateY'], + '0x409F': ['DS', '1', 'ChannelOverlap'], + '0x40A0': ['LO', '1', 'ImageQualityIndicatorType'], + '0x40A1': ['LO', '1', 'ImageQualityIndicatorMaterial'], + '0x40A2': ['LO', '1', 'ImageQualityIndicatorSize'], + '0x5002': ['IS', '1', 'LINACEnergy'], + '0x5004': ['IS', '1', 'LINACOutput'], + '0x5100': ['US', '1', 'ActiveAperture'], + '0x5101': ['DS', '1', 'TotalAperture'], + '0x5102': ['DS', '1', 'ApertureElevation'], + '0x5103': ['DS', '1', 'MainLobeAngle'], + '0x5104': ['DS', '1', 'MainRoofAngle'], + '0x5105': ['CS', '1', 'ConnectorType'], + '0x5106': ['SH', '1', 'WedgeModelNumber'], + '0x5107': ['DS', '1', 'WedgeAngleFloat'], + '0x5108': ['DS', '1', 'WedgeRoofAngle'], + '0x5109': ['CS', '1', 'WedgeElement1Position'], + '0x510A': ['DS', '1', 'WedgeMaterialVelocity'], + '0x510B': ['SH', '1', 'WedgeMaterial'], + '0x510C': ['DS', '1', 'WedgeOffsetZ'], + '0x510D': ['DS', '1', 'WedgeOriginOffsetX'], + '0x510E': ['DS', '1', 'WedgeTimeDelay'], + '0x510F': ['SH', '1', 'WedgeName'], + '0x5110': ['SH', '1', 'WedgeManufacturerName'], + '0x5111': ['LO', '1', 'WedgeDescription'], + '0x5112': ['DS', '1', 'NominalBeamAngle'], + '0x5113': ['DS', '1', 'WedgeOffsetX'], + '0x5114': ['DS', '1', 'WedgeOffsetY'], + '0x5115': ['DS', '1', 'WedgeTotalLength'], + '0x5116': ['DS', '1', 'WedgeInContactLength'], + '0x5117': ['DS', '1', 'WedgeFrontGap'], + '0x5118': ['DS', '1', 'WedgeTotalHeight'], + '0x5119': ['DS', '1', 'WedgeFrontHeight'], + '0x511A': ['DS', '1', 'WedgeRearHeight'], + '0x511B': ['DS', '1', 'WedgeTotalWidth'], + '0x511C': ['DS', '1', 'WedgeInContactWidth'], + '0x511D': ['DS', '1', 'WedgeChamferHeight'], + '0x511E': ['CS', '1', 'WedgeCurve'], + '0x511F': ['DS', '1', 'RadiusAlongWedge'] + }, + '0x0018': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['LO', '1', 'ContrastBolusAgent'], + '0x0012': ['SQ', '1', 'ContrastBolusAgentSequence'], + '0x0013': ['FL', '1', 'ContrastBolusT1Relaxivity'], + '0x0014': ['SQ', '1', 'ContrastBolusAdministrationRouteSequence'], + '0x0015': ['CS', '1', 'BodyPartExamined'], + '0x0020': ['CS', '1-n', 'ScanningSequence'], + '0x0021': ['CS', '1-n', 'SequenceVariant'], + '0x0022': ['CS', '1-n', 'ScanOptions'], + '0x0023': ['CS', '1', 'MRAcquisitionType'], + '0x0024': ['SH', '1', 'SequenceName'], + '0x0025': ['CS', '1', 'AngioFlag'], + '0x0026': ['SQ', '1', 'InterventionDrugInformationSequence'], + '0x0027': ['TM', '1', 'InterventionDrugStopTime'], + '0x0028': ['DS', '1', 'InterventionDrugDose'], + '0x0029': ['SQ', '1', 'InterventionDrugCodeSequence'], + '0x002A': ['SQ', '1', 'AdditionalDrugSequence'], + '0x0030': ['LO', '1-n', 'Radionuclide'], + '0x0031': ['LO', '1', 'Radiopharmaceutical'], + '0x0032': ['DS', '1', 'EnergyWindowCenterline'], + '0x0033': ['DS', '1-n', 'EnergyWindowTotalWidth'], + '0x0034': ['LO', '1', 'InterventionDrugName'], + '0x0035': ['TM', '1', 'InterventionDrugStartTime'], + '0x0036': ['SQ', '1', 'InterventionSequence'], + '0x0037': ['CS', '1', 'TherapyType'], + '0x0038': ['CS', '1', 'InterventionStatus'], + '0x0039': ['CS', '1', 'TherapyDescription'], + '0x003A': ['ST', '1', 'InterventionDescription'], + '0x0040': ['IS', '1', 'CineRate'], + '0x0042': ['CS', '1', 'InitialCineRunState'], + '0x0050': ['DS', '1', 'SliceThickness'], + '0x0060': ['DS', '1', 'KVP'], + '0x0070': ['IS', '1', 'CountsAccumulated'], + '0x0071': ['CS', '1', 'AcquisitionTerminationCondition'], + '0x0072': ['DS', '1', 'EffectiveDuration'], + '0x0073': ['CS', '1', 'AcquisitionStartCondition'], + '0x0074': ['IS', '1', 'AcquisitionStartConditionData'], + '0x0075': ['IS', '1', 'AcquisitionTerminationConditionData'], + '0x0080': ['DS', '1', 'RepetitionTime'], + '0x0081': ['DS', '1', 'EchoTime'], + '0x0082': ['DS', '1', 'InversionTime'], + '0x0083': ['DS', '1', 'NumberOfAverages'], + '0x0084': ['DS', '1', 'ImagingFrequency'], + '0x0085': ['SH', '1', 'ImagedNucleus'], + '0x0086': ['IS', '1-n', 'EchoNumbers'], + '0x0087': ['DS', '1', 'MagneticFieldStrength'], + '0x0088': ['DS', '1', 'SpacingBetweenSlices'], + '0x0089': ['IS', '1', 'NumberOfPhaseEncodingSteps'], + '0x0090': ['DS', '1', 'DataCollectionDiameter'], + '0x0091': ['IS', '1', 'EchoTrainLength'], + '0x0093': ['DS', '1', 'PercentSampling'], + '0x0094': ['DS', '1', 'PercentPhaseFieldOfView'], + '0x0095': ['DS', '1', 'PixelBandwidth'], + '0x1000': ['LO', '1', 'DeviceSerialNumber'], + '0x1002': ['UI', '1', 'DeviceUID'], + '0x1003': ['LO', '1', 'DeviceID'], + '0x1004': ['LO', '1', 'PlateID'], + '0x1005': ['LO', '1', 'GeneratorID'], + '0x1006': ['LO', '1', 'GridID'], + '0x1007': ['LO', '1', 'CassetteID'], + '0x1008': ['LO', '1', 'GantryID'], + '0x1010': ['LO', '1', 'SecondaryCaptureDeviceID'], + '0x1011': ['LO', '1', 'HardcopyCreationDeviceID'], + '0x1012': ['DA', '1', 'DateOfSecondaryCapture'], + '0x1014': ['TM', '1', 'TimeOfSecondaryCapture'], + '0x1016': ['LO', '1', 'SecondaryCaptureDeviceManufacturer'], + '0x1017': ['LO', '1', 'HardcopyDeviceManufacturer'], + '0x1018': ['LO', '1', 'SecondaryCaptureDeviceManufacturerModelName'], + '0x1019': ['LO', '1-n', 'SecondaryCaptureDeviceSoftwareVersions'], + '0x101A': ['LO', '1-n', 'HardcopyDeviceSoftwareVersion'], + '0x101B': ['LO', '1', 'HardcopyDeviceManufacturerModelName'], + '0x1020': ['LO', '1-n', 'SoftwareVersions'], + '0x1022': ['SH', '1', 'VideoImageFormatAcquired'], + '0x1023': ['LO', '1', 'DigitalImageFormatAcquired'], + '0x1030': ['LO', '1', 'ProtocolName'], + '0x1040': ['LO', '1', 'ContrastBolusRoute'], + '0x1041': ['DS', '1', 'ContrastBolusVolume'], + '0x1042': ['TM', '1', 'ContrastBolusStartTime'], + '0x1043': ['TM', '1', 'ContrastBolusStopTime'], + '0x1044': ['DS', '1', 'ContrastBolusTotalDose'], + '0x1045': ['IS', '1', 'SyringeCounts'], + '0x1046': ['DS', '1-n', 'ContrastFlowRate'], + '0x1047': ['DS', '1-n', 'ContrastFlowDuration'], + '0x1048': ['CS', '1', 'ContrastBolusIngredient'], + '0x1049': ['DS', '1', 'ContrastBolusIngredientConcentration'], + '0x1050': ['DS', '1', 'SpatialResolution'], + '0x1060': ['DS', '1', 'TriggerTime'], + '0x1061': ['LO', '1', 'TriggerSourceOrType'], + '0x1062': ['IS', '1', 'NominalInterval'], + '0x1063': ['DS', '1', 'FrameTime'], + '0x1064': ['LO', '1', 'CardiacFramingType'], + '0x1065': ['DS', '1-n', 'FrameTimeVector'], + '0x1066': ['DS', '1', 'FrameDelay'], + '0x1067': ['DS', '1', 'ImageTriggerDelay'], + '0x1068': ['DS', '1', 'MultiplexGroupTimeOffset'], + '0x1069': ['DS', '1', 'TriggerTimeOffset'], + '0x106A': ['CS', '1', 'SynchronizationTrigger'], + '0x106C': ['US', '2', 'SynchronizationChannel'], + '0x106E': ['UL', '1', 'TriggerSamplePosition'], + '0x1070': ['LO', '1', 'RadiopharmaceuticalRoute'], + '0x1071': ['DS', '1', 'RadiopharmaceuticalVolume'], + '0x1072': ['TM', '1', 'RadiopharmaceuticalStartTime'], + '0x1073': ['TM', '1', 'RadiopharmaceuticalStopTime'], + '0x1074': ['DS', '1', 'RadionuclideTotalDose'], + '0x1075': ['DS', '1', 'RadionuclideHalfLife'], + '0x1076': ['DS', '1', 'RadionuclidePositronFraction'], + '0x1077': ['DS', '1', 'RadiopharmaceuticalSpecificActivity'], + '0x1078': ['DT', '1', 'RadiopharmaceuticalStartDateTime'], + '0x1079': ['DT', '1', 'RadiopharmaceuticalStopDateTime'], + '0x1080': ['CS', '1', 'BeatRejectionFlag'], + '0x1081': ['IS', '1', 'LowRRValue'], + '0x1082': ['IS', '1', 'HighRRValue'], + '0x1083': ['IS', '1', 'IntervalsAcquired'], + '0x1084': ['IS', '1', 'IntervalsRejected'], + '0x1085': ['LO', '1', 'PVCRejection'], + '0x1086': ['IS', '1', 'SkipBeats'], + '0x1088': ['IS', '1', 'HeartRate'], + '0x1090': ['IS', '1', 'CardiacNumberOfImages'], + '0x1094': ['IS', '1', 'TriggerWindow'], + '0x1100': ['DS', '1', 'ReconstructionDiameter'], + '0x1110': ['DS', '1', 'DistanceSourceToDetector'], + '0x1111': ['DS', '1', 'DistanceSourceToPatient'], + '0x1114': ['DS', '1', 'EstimatedRadiographicMagnificationFactor'], + '0x1120': ['DS', '1', 'GantryDetectorTilt'], + '0x1121': ['DS', '1', 'GantryDetectorSlew'], + '0x1130': ['DS', '1', 'TableHeight'], + '0x1131': ['DS', '1', 'TableTraverse'], + '0x1134': ['CS', '1', 'TableMotion'], + '0x1135': ['DS', '1-n', 'TableVerticalIncrement'], + '0x1136': ['DS', '1-n', 'TableLateralIncrement'], + '0x1137': ['DS', '1-n', 'TableLongitudinalIncrement'], + '0x1138': ['DS', '1', 'TableAngle'], + '0x113A': ['CS', '1', 'TableType'], + '0x1140': ['CS', '1', 'RotationDirection'], + '0x1141': ['DS', '1', 'AngularPosition'], + '0x1142': ['DS', '1-n', 'RadialPosition'], + '0x1143': ['DS', '1', 'ScanArc'], + '0x1144': ['DS', '1', 'AngularStep'], + '0x1145': ['DS', '1', 'CenterOfRotationOffset'], + '0x1146': ['DS', '1-n', 'RotationOffset'], + '0x1147': ['CS', '1', 'FieldOfViewShape'], + '0x1149': ['IS', '1-2', 'FieldOfViewDimensions'], + '0x1150': ['IS', '1', 'ExposureTime'], + '0x1151': ['IS', '1', 'XRayTubeCurrent'], + '0x1152': ['IS', '1', 'Exposure'], + '0x1153': ['IS', '1', 'ExposureInuAs'], + '0x1154': ['DS', '1', 'AveragePulseWidth'], + '0x1155': ['CS', '1', 'RadiationSetting'], + '0x1156': ['CS', '1', 'RectificationType'], + '0x115A': ['CS', '1', 'RadiationMode'], + '0x115E': ['DS', '1', 'ImageAndFluoroscopyAreaDoseProduct'], + '0x1160': ['SH', '1', 'FilterType'], + '0x1161': ['LO', '1-n', 'TypeOfFilters'], + '0x1162': ['DS', '1', 'IntensifierSize'], + '0x1164': ['DS', '2', 'ImagerPixelSpacing'], + '0x1166': ['CS', '1-n', 'Grid'], + '0x1170': ['IS', '1', 'GeneratorPower'], + '0x1180': ['SH', '1', 'CollimatorGridName'], + '0x1181': ['CS', '1', 'CollimatorType'], + '0x1182': ['IS', '1-2', 'FocalDistance'], + '0x1183': ['DS', '1-2', 'XFocusCenter'], + '0x1184': ['DS', '1-2', 'YFocusCenter'], + '0x1190': ['DS', '1-n', 'FocalSpots'], + '0x1191': ['CS', '1', 'AnodeTargetMaterial'], + '0x11A0': ['DS', '1', 'BodyPartThickness'], + '0x11A2': ['DS', '1', 'CompressionForce'], + '0x11A4': ['LO', '1', 'PaddleDescription'], + '0x1200': ['DA', '1-n', 'DateOfLastCalibration'], + '0x1201': ['TM', '1-n', 'TimeOfLastCalibration'], + '0x1202': ['DT', '1', 'DateTimeOfLastCalibration'], + '0x1210': ['SH', '1-n', 'ConvolutionKernel'], + '0x1240': ['IS', '1-n', 'UpperLowerPixelValues'], + '0x1242': ['IS', '1', 'ActualFrameDuration'], + '0x1243': ['IS', '1', 'CountRate'], + '0x1244': ['US', '1', 'PreferredPlaybackSequencing'], + '0x1250': ['SH', '1', 'ReceiveCoilName'], + '0x1251': ['SH', '1', 'TransmitCoilName'], + '0x1260': ['SH', '1', 'PlateType'], + '0x1261': ['LO', '1', 'PhosphorType'], + '0x1300': ['DS', '1', 'ScanVelocity'], + '0x1301': ['CS', '1-n', 'WholeBodyTechnique'], + '0x1302': ['IS', '1', 'ScanLength'], + '0x1310': ['US', '4', 'AcquisitionMatrix'], + '0x1312': ['CS', '1', 'InPlanePhaseEncodingDirection'], + '0x1314': ['DS', '1', 'FlipAngle'], + '0x1315': ['CS', '1', 'VariableFlipAngleFlag'], + '0x1316': ['DS', '1', 'SAR'], + '0x1318': ['DS', '1', 'dBdt'], + '0x1400': ['LO', '1', 'AcquisitionDeviceProcessingDescription'], + '0x1401': ['LO', '1', 'AcquisitionDeviceProcessingCode'], + '0x1402': ['CS', '1', 'CassetteOrientation'], + '0x1403': ['CS', '1', 'CassetteSize'], + '0x1404': ['US', '1', 'ExposuresOnPlate'], + '0x1405': ['IS', '1', 'RelativeXRayExposure'], + '0x1411': ['DS', '1', 'ExposureIndex'], + '0x1412': ['DS', '1', 'TargetExposureIndex'], + '0x1413': ['DS', '1', 'DeviationIndex'], + '0x1450': ['DS', '1', 'ColumnAngulation'], + '0x1460': ['DS', '1', 'TomoLayerHeight'], + '0x1470': ['DS', '1', 'TomoAngle'], + '0x1480': ['DS', '1', 'TomoTime'], + '0x1490': ['CS', '1', 'TomoType'], + '0x1491': ['CS', '1', 'TomoClass'], + '0x1495': ['IS', '1', 'NumberOfTomosynthesisSourceImages'], + '0x1500': ['CS', '1', 'PositionerMotion'], + '0x1508': ['CS', '1', 'PositionerType'], + '0x1510': ['DS', '1', 'PositionerPrimaryAngle'], + '0x1511': ['DS', '1', 'PositionerSecondaryAngle'], + '0x1520': ['DS', '1-n', 'PositionerPrimaryAngleIncrement'], + '0x1521': ['DS', '1-n', 'PositionerSecondaryAngleIncrement'], + '0x1530': ['DS', '1', 'DetectorPrimaryAngle'], + '0x1531': ['DS', '1', 'DetectorSecondaryAngle'], + '0x1600': ['CS', '1-3', 'ShutterShape'], + '0x1602': ['IS', '1', 'ShutterLeftVerticalEdge'], + '0x1604': ['IS', '1', 'ShutterRightVerticalEdge'], + '0x1606': ['IS', '1', 'ShutterUpperHorizontalEdge'], + '0x1608': ['IS', '1', 'ShutterLowerHorizontalEdge'], + '0x1610': ['IS', '2', 'CenterOfCircularShutter'], + '0x1612': ['IS', '1', 'RadiusOfCircularShutter'], + '0x1620': ['IS', '2-2n', 'VerticesOfThePolygonalShutter'], + '0x1622': ['US', '1', 'ShutterPresentationValue'], + '0x1623': ['US', '1', 'ShutterOverlayGroup'], + '0x1624': ['US', '3', 'ShutterPresentationColorCIELabValue'], + '0x1700': ['CS', '1-3', 'CollimatorShape'], + '0x1702': ['IS', '1', 'CollimatorLeftVerticalEdge'], + '0x1704': ['IS', '1', 'CollimatorRightVerticalEdge'], + '0x1706': ['IS', '1', 'CollimatorUpperHorizontalEdge'], + '0x1708': ['IS', '1', 'CollimatorLowerHorizontalEdge'], + '0x1710': ['IS', '2', 'CenterOfCircularCollimator'], + '0x1712': ['IS', '1', 'RadiusOfCircularCollimator'], + '0x1720': ['IS', '2-2n', 'VerticesOfThePolygonalCollimator'], + '0x1800': ['CS', '1', 'AcquisitionTimeSynchronized'], + '0x1801': ['SH', '1', 'TimeSource'], + '0x1802': ['CS', '1', 'TimeDistributionProtocol'], + '0x1803': ['LO', '1', 'NTPSourceAddress'], + '0x2001': ['IS', '1-n', 'PageNumberVector'], + '0x2002': ['SH', '1-n', 'FrameLabelVector'], + '0x2003': ['DS', '1-n', 'FramePrimaryAngleVector'], + '0x2004': ['DS', '1-n', 'FrameSecondaryAngleVector'], + '0x2005': ['DS', '1-n', 'SliceLocationVector'], + '0x2006': ['SH', '1-n', 'DisplayWindowLabelVector'], + '0x2010': ['DS', '2', 'NominalScannedPixelSpacing'], + '0x2020': ['CS', '1', 'DigitizingDeviceTransportDirection'], + '0x2030': ['DS', '1', 'RotationOfScannedFilm'], + '0x2041': ['SQ', '1', 'BiopsyTargetSequence'], + '0x2042': ['UI', '1', 'TargetUID'], + '0x2043': ['FL', '2', 'LocalizingCursorPosition'], + '0x2044': ['FL', '3', 'CalculatedTargetPosition'], + '0x2045': ['SH', '1', 'TargetLabel'], + '0x2046': ['FL', '1', 'DisplayedZValue'], + '0x3100': ['CS', '1', 'IVUSAcquisition'], + '0x3101': ['DS', '1', 'IVUSPullbackRate'], + '0x3102': ['DS', '1', 'IVUSGatedRate'], + '0x3103': ['IS', '1', 'IVUSPullbackStartFrameNumber'], + '0x3104': ['IS', '1', 'IVUSPullbackStopFrameNumber'], + '0x3105': ['IS', '1-n', 'LesionNumber'], + '0x4000': ['LT', '1', 'AcquisitionComments'], + '0x5000': ['SH', '1-n', 'OutputPower'], + '0x5010': ['LO', '1-n', 'TransducerData'], + '0x5012': ['DS', '1', 'FocusDepth'], + '0x5020': ['LO', '1', 'ProcessingFunction'], + '0x5021': ['LO', '1', 'PostprocessingFunction'], + '0x5022': ['DS', '1', 'MechanicalIndex'], + '0x5024': ['DS', '1', 'BoneThermalIndex'], + '0x5026': ['DS', '1', 'CranialThermalIndex'], + '0x5027': ['DS', '1', 'SoftTissueThermalIndex'], + '0x5028': ['DS', '1', 'SoftTissueFocusThermalIndex'], + '0x5029': ['DS', '1', 'SoftTissueSurfaceThermalIndex'], + '0x5030': ['DS', '1', 'DynamicRange'], + '0x5040': ['DS', '1', 'TotalGain'], + '0x5050': ['IS', '1', 'DepthOfScanField'], + '0x5100': ['CS', '1', 'PatientPosition'], + '0x5101': ['CS', '1', 'ViewPosition'], + '0x5104': ['SQ', '1', 'ProjectionEponymousNameCodeSequence'], + '0x5210': ['DS', '6', 'ImageTransformationMatrix'], + '0x5212': ['DS', '3', 'ImageTranslationVector'], + '0x6000': ['DS', '1', 'Sensitivity'], + '0x6011': ['SQ', '1', 'SequenceOfUltrasoundRegions'], + '0x6012': ['US', '1', 'RegionSpatialFormat'], + '0x6014': ['US', '1', 'RegionDataType'], + '0x6016': ['UL', '1', 'RegionFlags'], + '0x6018': ['UL', '1', 'RegionLocationMinX0'], + '0x601A': ['UL', '1', 'RegionLocationMinY0'], + '0x601C': ['UL', '1', 'RegionLocationMaxX1'], + '0x601E': ['UL', '1', 'RegionLocationMaxY1'], + '0x6020': ['SL', '1', 'ReferencePixelX0'], + '0x6022': ['SL', '1', 'ReferencePixelY0'], + '0x6024': ['US', '1', 'PhysicalUnitsXDirection'], + '0x6026': ['US', '1', 'PhysicalUnitsYDirection'], + '0x6028': ['FD', '1', 'ReferencePixelPhysicalValueX'], + '0x602A': ['FD', '1', 'ReferencePixelPhysicalValueY'], + '0x602C': ['FD', '1', 'PhysicalDeltaX'], + '0x602E': ['FD', '1', 'PhysicalDeltaY'], + '0x6030': ['UL', '1', 'TransducerFrequency'], + '0x6031': ['CS', '1', 'TransducerType'], + '0x6032': ['UL', '1', 'PulseRepetitionFrequency'], + '0x6034': ['FD', '1', 'DopplerCorrectionAngle'], + '0x6036': ['FD', '1', 'SteeringAngle'], + '0x6038': ['UL', '1', 'DopplerSampleVolumeXPositionRetired'], + '0x6039': ['SL', '1', 'DopplerSampleVolumeXPosition'], + '0x603A': ['UL', '1', 'DopplerSampleVolumeYPositionRetired'], + '0x603B': ['SL', '1', 'DopplerSampleVolumeYPosition'], + '0x603C': ['UL', '1', 'TMLinePositionX0Retired'], + '0x603D': ['SL', '1', 'TMLinePositionX0'], + '0x603E': ['UL', '1', 'TMLinePositionY0Retired'], + '0x603F': ['SL', '1', 'TMLinePositionY0'], + '0x6040': ['UL', '1', 'TMLinePositionX1Retired'], + '0x6041': ['SL', '1', 'TMLinePositionX1'], + '0x6042': ['UL', '1', 'TMLinePositionY1Retired'], + '0x6043': ['SL', '1', 'TMLinePositionY1'], + '0x6044': ['US', '1', 'PixelComponentOrganization'], + '0x6046': ['UL', '1', 'PixelComponentMask'], + '0x6048': ['UL', '1', 'PixelComponentRangeStart'], + '0x604A': ['UL', '1', 'PixelComponentRangeStop'], + '0x604C': ['US', '1', 'PixelComponentPhysicalUnits'], + '0x604E': ['US', '1', 'PixelComponentDataType'], + '0x6050': ['UL', '1', 'NumberOfTableBreakPoints'], + '0x6052': ['UL', '1-n', 'TableOfXBreakPoints'], + '0x6054': ['FD', '1-n', 'TableOfYBreakPoints'], + '0x6056': ['UL', '1', 'NumberOfTableEntries'], + '0x6058': ['UL', '1-n', 'TableOfPixelValues'], + '0x605A': ['FL', '1-n', 'TableOfParameterValues'], + '0x6060': ['FL', '1-n', 'RWaveTimeVector'], + '0x7000': ['CS', '1', 'DetectorConditionsNominalFlag'], + '0x7001': ['DS', '1', 'DetectorTemperature'], + '0x7004': ['CS', '1', 'DetectorType'], + '0x7005': ['CS', '1', 'DetectorConfiguration'], + '0x7006': ['LT', '1', 'DetectorDescription'], + '0x7008': ['LT', '1', 'DetectorMode'], + '0x700A': ['SH', '1', 'DetectorID'], + '0x700C': ['DA', '1', 'DateOfLastDetectorCalibration'], + '0x700E': ['TM', '1', 'TimeOfLastDetectorCalibration'], + '0x7010': ['IS', '1', 'ExposuresOnDetectorSinceLastCalibration'], + '0x7011': ['IS', '1', 'ExposuresOnDetectorSinceManufactured'], + '0x7012': ['DS', '1', 'DetectorTimeSinceLastExposure'], + '0x7014': ['DS', '1', 'DetectorActiveTime'], + '0x7016': ['DS', '1', 'DetectorActivationOffsetFromExposure'], + '0x701A': ['DS', '2', 'DetectorBinning'], + '0x7020': ['DS', '2', 'DetectorElementPhysicalSize'], + '0x7022': ['DS', '2', 'DetectorElementSpacing'], + '0x7024': ['CS', '1', 'DetectorActiveShape'], + '0x7026': ['DS', '1-2', 'DetectorActiveDimensions'], + '0x7028': ['DS', '2', 'DetectorActiveOrigin'], + '0x702A': ['LO', '1', 'DetectorManufacturerName'], + '0x702B': ['LO', '1', 'DetectorManufacturerModelName'], + '0x7030': ['DS', '2', 'FieldOfViewOrigin'], + '0x7032': ['DS', '1', 'FieldOfViewRotation'], + '0x7034': ['CS', '1', 'FieldOfViewHorizontalFlip'], + '0x7036': ['FL', '2', 'PixelDataAreaOriginRelativeToFOV'], + '0x7038': ['FL', '1', 'PixelDataAreaRotationAngleRelativeToFOV'], + '0x7040': ['LT', '1', 'GridAbsorbingMaterial'], + '0x7041': ['LT', '1', 'GridSpacingMaterial'], + '0x7042': ['DS', '1', 'GridThickness'], + '0x7044': ['DS', '1', 'GridPitch'], + '0x7046': ['IS', '2', 'GridAspectRatio'], + '0x7048': ['DS', '1', 'GridPeriod'], + '0x704C': ['DS', '1', 'GridFocalDistance'], + '0x7050': ['CS', '1-n', 'FilterMaterial'], + '0x7052': ['DS', '1-n', 'FilterThicknessMinimum'], + '0x7054': ['DS', '1-n', 'FilterThicknessMaximum'], + '0x7056': ['FL', '1-n', 'FilterBeamPathLengthMinimum'], + '0x7058': ['FL', '1-n', 'FilterBeamPathLengthMaximum'], + '0x7060': ['CS', '1', 'ExposureControlMode'], + '0x7062': ['LT', '1', 'ExposureControlModeDescription'], + '0x7064': ['CS', '1', 'ExposureStatus'], + '0x7065': ['DS', '1', 'PhototimerSetting'], + '0x8150': ['DS', '1', 'ExposureTimeInuS'], + '0x8151': ['DS', '1', 'XRayTubeCurrentInuA'], + '0x9004': ['CS', '1', 'ContentQualification'], + '0x9005': ['SH', '1', 'PulseSequenceName'], + '0x9006': ['SQ', '1', 'MRImagingModifierSequence'], + '0x9008': ['CS', '1', 'EchoPulseSequence'], + '0x9009': ['CS', '1', 'InversionRecovery'], + '0x9010': ['CS', '1', 'FlowCompensation'], + '0x9011': ['CS', '1', 'MultipleSpinEcho'], + '0x9012': ['CS', '1', 'MultiPlanarExcitation'], + '0x9014': ['CS', '1', 'PhaseContrast'], + '0x9015': ['CS', '1', 'TimeOfFlightContrast'], + '0x9016': ['CS', '1', 'Spoiling'], + '0x9017': ['CS', '1', 'SteadyStatePulseSequence'], + '0x9018': ['CS', '1', 'EchoPlanarPulseSequence'], + '0x9019': ['FD', '1', 'TagAngleFirstAxis'], + '0x9020': ['CS', '1', 'MagnetizationTransfer'], + '0x9021': ['CS', '1', 'T2Preparation'], + '0x9022': ['CS', '1', 'BloodSignalNulling'], + '0x9024': ['CS', '1', 'SaturationRecovery'], + '0x9025': ['CS', '1', 'SpectrallySelectedSuppression'], + '0x9026': ['CS', '1', 'SpectrallySelectedExcitation'], + '0x9027': ['CS', '1', 'SpatialPresaturation'], + '0x9028': ['CS', '1', 'Tagging'], + '0x9029': ['CS', '1', 'OversamplingPhase'], + '0x9030': ['FD', '1', 'TagSpacingFirstDimension'], + '0x9032': ['CS', '1', 'GeometryOfKSpaceTraversal'], + '0x9033': ['CS', '1', 'SegmentedKSpaceTraversal'], + '0x9034': ['CS', '1', 'RectilinearPhaseEncodeReordering'], + '0x9035': ['FD', '1', 'TagThickness'], + '0x9036': ['CS', '1', 'PartialFourierDirection'], + '0x9037': ['CS', '1', 'CardiacSynchronizationTechnique'], + '0x9041': ['LO', '1', 'ReceiveCoilManufacturerName'], + '0x9042': ['SQ', '1', 'MRReceiveCoilSequence'], + '0x9043': ['CS', '1', 'ReceiveCoilType'], + '0x9044': ['CS', '1', 'QuadratureReceiveCoil'], + '0x9045': ['SQ', '1', 'MultiCoilDefinitionSequence'], + '0x9046': ['LO', '1', 'MultiCoilConfiguration'], + '0x9047': ['SH', '1', 'MultiCoilElementName'], + '0x9048': ['CS', '1', 'MultiCoilElementUsed'], + '0x9049': ['SQ', '1', 'MRTransmitCoilSequence'], + '0x9050': ['LO', '1', 'TransmitCoilManufacturerName'], + '0x9051': ['CS', '1', 'TransmitCoilType'], + '0x9052': ['FD', '1-2', 'SpectralWidth'], + '0x9053': ['FD', '1-2', 'ChemicalShiftReference'], + '0x9054': ['CS', '1', 'VolumeLocalizationTechnique'], + '0x9058': ['US', '1', 'MRAcquisitionFrequencyEncodingSteps'], + '0x9059': ['CS', '1', 'Decoupling'], + '0x9060': ['CS', '1-2', 'DecoupledNucleus'], + '0x9061': ['FD', '1-2', 'DecouplingFrequency'], + '0x9062': ['CS', '1', 'DecouplingMethod'], + '0x9063': ['FD', '1-2', 'DecouplingChemicalShiftReference'], + '0x9064': ['CS', '1', 'KSpaceFiltering'], + '0x9065': ['CS', '1-2', 'TimeDomainFiltering'], + '0x9066': ['US', '1-2', 'NumberOfZeroFills'], + '0x9067': ['CS', '1', 'BaselineCorrection'], + '0x9069': ['FD', '1', 'ParallelReductionFactorInPlane'], + '0x9070': ['FD', '1', 'CardiacRRIntervalSpecified'], + '0x9073': ['FD', '1', 'AcquisitionDuration'], + '0x9074': ['DT', '1', 'FrameAcquisitionDateTime'], + '0x9075': ['CS', '1', 'DiffusionDirectionality'], + '0x9076': ['SQ', '1', 'DiffusionGradientDirectionSequence'], + '0x9077': ['CS', '1', 'ParallelAcquisition'], + '0x9078': ['CS', '1', 'ParallelAcquisitionTechnique'], + '0x9079': ['FD', '1-n', 'InversionTimes'], + '0x9080': ['ST', '1', 'MetaboliteMapDescription'], + '0x9081': ['CS', '1', 'PartialFourier'], + '0x9082': ['FD', '1', 'EffectiveEchoTime'], + '0x9083': ['SQ', '1', 'MetaboliteMapCodeSequence'], + '0x9084': ['SQ', '1', 'ChemicalShiftSequence'], + '0x9085': ['CS', '1', 'CardiacSignalSource'], + '0x9087': ['FD', '1', 'DiffusionBValue'], + '0x9089': ['FD', '3', 'DiffusionGradientOrientation'], + '0x9090': ['FD', '3', 'VelocityEncodingDirection'], + '0x9091': ['FD', '1', 'VelocityEncodingMinimumValue'], + '0x9092': ['SQ', '1', 'VelocityEncodingAcquisitionSequence'], + '0x9093': ['US', '1', 'NumberOfKSpaceTrajectories'], + '0x9094': ['CS', '1', 'CoverageOfKSpace'], + '0x9095': ['UL', '1', 'SpectroscopyAcquisitionPhaseRows'], + '0x9096': ['FD', '1', 'ParallelReductionFactorInPlaneRetired'], + '0x9098': ['FD', '1-2', 'TransmitterFrequency'], + '0x9100': ['CS', '1-2', 'ResonantNucleus'], + '0x9101': ['CS', '1', 'FrequencyCorrection'], + '0x9103': ['SQ', '1', 'MRSpectroscopyFOVGeometrySequence'], + '0x9104': ['FD', '1', 'SlabThickness'], + '0x9105': ['FD', '3', 'SlabOrientation'], + '0x9106': ['FD', '3', 'MidSlabPosition'], + '0x9107': ['SQ', '1', 'MRSpatialSaturationSequence'], + '0x9112': ['SQ', '1', 'MRTimingAndRelatedParametersSequence'], + '0x9114': ['SQ', '1', 'MREchoSequence'], + '0x9115': ['SQ', '1', 'MRModifierSequence'], + '0x9117': ['SQ', '1', 'MRDiffusionSequence'], + '0x9118': ['SQ', '1', 'CardiacSynchronizationSequence'], + '0x9119': ['SQ', '1', 'MRAveragesSequence'], + '0x9125': ['SQ', '1', 'MRFOVGeometrySequence'], + '0x9126': ['SQ', '1', 'VolumeLocalizationSequence'], + '0x9127': ['UL', '1', 'SpectroscopyAcquisitionDataColumns'], + '0x9147': ['CS', '1', 'DiffusionAnisotropyType'], + '0x9151': ['DT', '1', 'FrameReferenceDateTime'], + '0x9152': ['SQ', '1', 'MRMetaboliteMapSequence'], + '0x9155': ['FD', '1', 'ParallelReductionFactorOutOfPlane'], + '0x9159': ['UL', '1', 'SpectroscopyAcquisitionOutOfPlanePhaseSteps'], + '0x9166': ['CS', '1', 'BulkMotionStatus'], + '0x9168': ['FD', '1', 'ParallelReductionFactorSecondInPlane'], + '0x9169': ['CS', '1', 'CardiacBeatRejectionTechnique'], + '0x9170': ['CS', '1', 'RespiratoryMotionCompensationTechnique'], + '0x9171': ['CS', '1', 'RespiratorySignalSource'], + '0x9172': ['CS', '1', 'BulkMotionCompensationTechnique'], + '0x9173': ['CS', '1', 'BulkMotionSignalSource'], + '0x9174': ['CS', '1', 'ApplicableSafetyStandardAgency'], + '0x9175': ['LO', '1', 'ApplicableSafetyStandardDescription'], + '0x9176': ['SQ', '1', 'OperatingModeSequence'], + '0x9177': ['CS', '1', 'OperatingModeType'], + '0x9178': ['CS', '1', 'OperatingMode'], + '0x9179': ['CS', '1', 'SpecificAbsorptionRateDefinition'], + '0x9180': ['CS', '1', 'GradientOutputType'], + '0x9181': ['FD', '1', 'SpecificAbsorptionRateValue'], + '0x9182': ['FD', '1', 'GradientOutput'], + '0x9183': ['CS', '1', 'FlowCompensationDirection'], + '0x9184': ['FD', '1', 'TaggingDelay'], + '0x9185': ['ST', '1', 'RespiratoryMotionCompensationTechniqueDescription'], + '0x9186': ['SH', '1', 'RespiratorySignalSourceID'], + '0x9195': ['FD', '1', 'ChemicalShiftMinimumIntegrationLimitInHz'], + '0x9196': ['FD', '1', 'ChemicalShiftMaximumIntegrationLimitInHz'], + '0x9197': ['SQ', '1', 'MRVelocityEncodingSequence'], + '0x9198': ['CS', '1', 'FirstOrderPhaseCorrection'], + '0x9199': ['CS', '1', 'WaterReferencedPhaseCorrection'], + '0x9200': ['CS', '1', 'MRSpectroscopyAcquisitionType'], + '0x9214': ['CS', '1', 'RespiratoryCyclePosition'], + '0x9217': ['FD', '1', 'VelocityEncodingMaximumValue'], + '0x9218': ['FD', '1', 'TagSpacingSecondDimension'], + '0x9219': ['SS', '1', 'TagAngleSecondAxis'], + '0x9220': ['FD', '1', 'FrameAcquisitionDuration'], + '0x9226': ['SQ', '1', 'MRImageFrameTypeSequence'], + '0x9227': ['SQ', '1', 'MRSpectroscopyFrameTypeSequence'], + '0x9231': ['US', '1', 'MRAcquisitionPhaseEncodingStepsInPlane'], + '0x9232': ['US', '1', 'MRAcquisitionPhaseEncodingStepsOutOfPlane'], + '0x9234': ['UL', '1', 'SpectroscopyAcquisitionPhaseColumns'], + '0x9236': ['CS', '1', 'CardiacCyclePosition'], + '0x9239': ['SQ', '1', 'SpecificAbsorptionRateSequence'], + '0x9240': ['US', '1', 'RFEchoTrainLength'], + '0x9241': ['US', '1', 'GradientEchoTrainLength'], + '0x9250': ['CS', '1', 'ArterialSpinLabelingContrast'], + '0x9251': ['SQ', '1', 'MRArterialSpinLabelingSequence'], + '0x9252': ['LO', '1', 'ASLTechniqueDescription'], + '0x9253': ['US', '1', 'ASLSlabNumber'], + '0x9254': ['FD', '1', 'ASLSlabThickness'], + '0x9255': ['FD', '3', 'ASLSlabOrientation'], + '0x9256': ['FD', '3', 'ASLMidSlabPosition'], + '0x9257': ['CS', '1', 'ASLContext'], + '0x9258': ['UL', '1', 'ASLPulseTrainDuration'], + '0x9259': ['CS', '1', 'ASLCrusherFlag'], + '0x925A': ['FD', '1', 'ASLCrusherFlowLimit'], + '0x925B': ['LO', '1', 'ASLCrusherDescription'], + '0x925C': ['CS', '1', 'ASLBolusCutoffFlag'], + '0x925D': ['SQ', '1', 'ASLBolusCutoffTimingSequence'], + '0x925E': ['LO', '1', 'ASLBolusCutoffTechnique'], + '0x925F': ['UL', '1', 'ASLBolusCutoffDelayTime'], + '0x9260': ['SQ', '1', 'ASLSlabSequence'], + '0x9295': ['FD', '1', 'ChemicalShiftMinimumIntegrationLimitInppm'], + '0x9296': ['FD', '1', 'ChemicalShiftMaximumIntegrationLimitInppm'], + '0x9297': ['CS', '1', 'WaterReferenceAcquisition'], + '0x9298': ['IS', '1', 'EchoPeakPosition'], + '0x9301': ['SQ', '1', 'CTAcquisitionTypeSequence'], + '0x9302': ['CS', '1', 'AcquisitionType'], + '0x9303': ['FD', '1', 'TubeAngle'], + '0x9304': ['SQ', '1', 'CTAcquisitionDetailsSequence'], + '0x9305': ['FD', '1', 'RevolutionTime'], + '0x9306': ['FD', '1', 'SingleCollimationWidth'], + '0x9307': ['FD', '1', 'TotalCollimationWidth'], + '0x9308': ['SQ', '1', 'CTTableDynamicsSequence'], + '0x9309': ['FD', '1', 'TableSpeed'], + '0x9310': ['FD', '1', 'TableFeedPerRotation'], + '0x9311': ['FD', '1', 'SpiralPitchFactor'], + '0x9312': ['SQ', '1', 'CTGeometrySequence'], + '0x9313': ['FD', '3', 'DataCollectionCenterPatient'], + '0x9314': ['SQ', '1', 'CTReconstructionSequence'], + '0x9315': ['CS', '1', 'ReconstructionAlgorithm'], + '0x9316': ['CS', '1', 'ConvolutionKernelGroup'], + '0x9317': ['FD', '2', 'ReconstructionFieldOfView'], + '0x9318': ['FD', '3', 'ReconstructionTargetCenterPatient'], + '0x9319': ['FD', '1', 'ReconstructionAngle'], + '0x9320': ['SH', '1', 'ImageFilter'], + '0x9321': ['SQ', '1', 'CTExposureSequence'], + '0x9322': ['FD', '2', 'ReconstructionPixelSpacing'], + '0x9323': ['CS', '1', 'ExposureModulationType'], + '0x9324': ['FD', '1', 'EstimatedDoseSaving'], + '0x9325': ['SQ', '1', 'CTXRayDetailsSequence'], + '0x9326': ['SQ', '1', 'CTPositionSequence'], + '0x9327': ['FD', '1', 'TablePosition'], + '0x9328': ['FD', '1', 'ExposureTimeInms'], + '0x9329': ['SQ', '1', 'CTImageFrameTypeSequence'], + '0x9330': ['FD', '1', 'XRayTubeCurrentInmA'], + '0x9332': ['FD', '1', 'ExposureInmAs'], + '0x9333': ['CS', '1', 'ConstantVolumeFlag'], + '0x9334': ['CS', '1', 'FluoroscopyFlag'], + '0x9335': ['FD', '1', 'DistanceSourceToDataCollectionCenter'], + '0x9337': ['US', '1', 'ContrastBolusAgentNumber'], + '0x9338': ['SQ', '1', 'ContrastBolusIngredientCodeSequence'], + '0x9340': ['SQ', '1', 'ContrastAdministrationProfileSequence'], + '0x9341': ['SQ', '1', 'ContrastBolusUsageSequence'], + '0x9342': ['CS', '1', 'ContrastBolusAgentAdministered'], + '0x9343': ['CS', '1', 'ContrastBolusAgentDetected'], + '0x9344': ['CS', '1', 'ContrastBolusAgentPhase'], + '0x9345': ['FD', '1', 'CTDIvol'], + '0x9346': ['SQ', '1', 'CTDIPhantomTypeCodeSequence'], + '0x9351': ['FL', '1', 'CalciumScoringMassFactorPatient'], + '0x9352': ['FL', '3', 'CalciumScoringMassFactorDevice'], + '0x9353': ['FL', '1', 'EnergyWeightingFactor'], + '0x9360': ['SQ', '1', 'CTAdditionalXRaySourceSequence'], + '0x9401': ['SQ', '1', 'ProjectionPixelCalibrationSequence'], + '0x9402': ['FL', '1', 'DistanceSourceToIsocenter'], + '0x9403': ['FL', '1', 'DistanceObjectToTableTop'], + '0x9404': ['FL', '2', 'ObjectPixelSpacingInCenterOfBeam'], + '0x9405': ['SQ', '1', 'PositionerPositionSequence'], + '0x9406': ['SQ', '1', 'TablePositionSequence'], + '0x9407': ['SQ', '1', 'CollimatorShapeSequence'], + '0x9410': ['CS', '1', 'PlanesInAcquisition'], + '0x9412': ['SQ', '1', 'XAXRFFrameCharacteristicsSequence'], + '0x9417': ['SQ', '1', 'FrameAcquisitionSequence'], + '0x9420': ['CS', '1', 'XRayReceptorType'], + '0x9423': ['LO', '1', 'AcquisitionProtocolName'], + '0x9424': ['LT', '1', 'AcquisitionProtocolDescription'], + '0x9425': ['CS', '1', 'ContrastBolusIngredientOpaque'], + '0x9426': ['FL', '1', 'DistanceReceptorPlaneToDetectorHousing'], + '0x9427': ['CS', '1', 'IntensifierActiveShape'], + '0x9428': ['FL', '1-2', 'IntensifierActiveDimensions'], + '0x9429': ['FL', '2', 'PhysicalDetectorSize'], + '0x9430': ['FL', '2', 'PositionOfIsocenterProjection'], + '0x9432': ['SQ', '1', 'FieldOfViewSequence'], + '0x9433': ['LO', '1', 'FieldOfViewDescription'], + '0x9434': ['SQ', '1', 'ExposureControlSensingRegionsSequence'], + '0x9435': ['CS', '1', 'ExposureControlSensingRegionShape'], + '0x9436': ['SS', '1', 'ExposureControlSensingRegionLeftVerticalEdge'], + '0x9437': ['SS', '1', 'ExposureControlSensingRegionRightVerticalEdge'], + '0x9438': ['SS', '1', 'ExposureControlSensingRegionUpperHorizontalEdge'], + '0x9439': ['SS', '1', 'ExposureControlSensingRegionLowerHorizontalEdge'], + '0x9440': ['SS', '2', 'CenterOfCircularExposureControlSensingRegion'], + '0x9441': ['US', '1', 'RadiusOfCircularExposureControlSensingRegion'], + '0x9442': ['SS', '2-n', 'VerticesOfThePolygonalExposureControlSensingRegion'], + '0x9445': ['', '', ''], + '0x9447': ['FL', '1', 'ColumnAngulationPatient'], + '0x9449': ['FL', '1', 'BeamAngle'], + '0x9451': ['SQ', '1', 'FrameDetectorParametersSequence'], + '0x9452': ['FL', '1', 'CalculatedAnatomyThickness'], + '0x9455': ['SQ', '1', 'CalibrationSequence'], + '0x9456': ['SQ', '1', 'ObjectThicknessSequence'], + '0x9457': ['CS', '1', 'PlaneIdentification'], + '0x9461': ['FL', '1-2', 'FieldOfViewDimensionsInFloat'], + '0x9462': ['SQ', '1', 'IsocenterReferenceSystemSequence'], + '0x9463': ['FL', '1', 'PositionerIsocenterPrimaryAngle'], + '0x9464': ['FL', '1', 'PositionerIsocenterSecondaryAngle'], + '0x9465': ['FL', '1', 'PositionerIsocenterDetectorRotationAngle'], + '0x9466': ['FL', '1', 'TableXPositionToIsocenter'], + '0x9467': ['FL', '1', 'TableYPositionToIsocenter'], + '0x9468': ['FL', '1', 'TableZPositionToIsocenter'], + '0x9469': ['FL', '1', 'TableHorizontalRotationAngle'], + '0x9470': ['FL', '1', 'TableHeadTiltAngle'], + '0x9471': ['FL', '1', 'TableCradleTiltAngle'], + '0x9472': ['SQ', '1', 'FrameDisplayShutterSequence'], + '0x9473': ['FL', '1', 'AcquiredImageAreaDoseProduct'], + '0x9474': ['CS', '1', 'CArmPositionerTabletopRelationship'], + '0x9476': ['SQ', '1', 'XRayGeometrySequence'], + '0x9477': ['SQ', '1', 'IrradiationEventIdentificationSequence'], + '0x9504': ['SQ', '1', 'XRay3DFrameTypeSequence'], + '0x9506': ['SQ', '1', 'ContributingSourcesSequence'], + '0x9507': ['SQ', '1', 'XRay3DAcquisitionSequence'], + '0x9508': ['FL', '1', 'PrimaryPositionerScanArc'], + '0x9509': ['FL', '1', 'SecondaryPositionerScanArc'], + '0x9510': ['FL', '1', 'PrimaryPositionerScanStartAngle'], + '0x9511': ['FL', '1', 'SecondaryPositionerScanStartAngle'], + '0x9514': ['FL', '1', 'PrimaryPositionerIncrement'], + '0x9515': ['FL', '1', 'SecondaryPositionerIncrement'], + '0x9516': ['DT', '1', 'StartAcquisitionDateTime'], + '0x9517': ['DT', '1', 'EndAcquisitionDateTime'], + '0x9518': ['SS', '1', 'PrimaryPositionerIncrementSign'], + '0x9519': ['SS', '1', 'SecondaryPositionerIncrementSign'], + '0x9524': ['LO', '1', 'ApplicationName'], + '0x9525': ['LO', '1', 'ApplicationVersion'], + '0x9526': ['LO', '1', 'ApplicationManufacturer'], + '0x9527': ['CS', '1', 'AlgorithmType'], + '0x9528': ['LO', '1', 'AlgorithmDescription'], + '0x9530': ['SQ', '1', 'XRay3DReconstructionSequence'], + '0x9531': ['LO', '1', 'ReconstructionDescription'], + '0x9538': ['SQ', '1', 'PerProjectionAcquisitionSequence'], + '0x9541': ['SQ', '1', 'DetectorPositionSequence'], + '0x9542': ['SQ', '1', 'XRayAcquisitionDoseSequence'], + '0x9543': ['FD', '1', 'XRaySourceIsocenterPrimaryAngle'], + '0x9544': ['FD', '1', 'XRaySourceIsocenterSecondaryAngle'], + '0x9545': ['FD', '1', 'BreastSupportIsocenterPrimaryAngle'], + '0x9546': ['FD', '1', 'BreastSupportIsocenterSecondaryAngle'], + '0x9547': ['FD', '1', 'BreastSupportXPositionToIsocenter'], + '0x9548': ['FD', '1', 'BreastSupportYPositionToIsocenter'], + '0x9549': ['FD', '1', 'BreastSupportZPositionToIsocenter'], + '0x9550': ['FD', '1', 'DetectorIsocenterPrimaryAngle'], + '0x9551': ['FD', '1', 'DetectorIsocenterSecondaryAngle'], + '0x9552': ['FD', '1', 'DetectorXPositionToIsocenter'], + '0x9553': ['FD', '1', 'DetectorYPositionToIsocenter'], + '0x9554': ['FD', '1', 'DetectorZPositionToIsocenter'], + '0x9555': ['SQ', '1', 'XRayGridSequence'], + '0x9556': ['SQ', '1', 'XRayFilterSequence'], + '0x9557': ['FD', '3', 'DetectorActiveAreaTLHCPosition'], + '0x9558': ['FD', '6', 'DetectorActiveAreaOrientation'], + '0x9559': ['CS', '1', 'PositionerPrimaryAngleDirection'], + '0x9601': ['SQ', '1', 'DiffusionBMatrixSequence'], + '0x9602': ['FD', '1', 'DiffusionBValueXX'], + '0x9603': ['FD', '1', 'DiffusionBValueXY'], + '0x9604': ['FD', '1', 'DiffusionBValueXZ'], + '0x9605': ['FD', '1', 'DiffusionBValueYY'], + '0x9606': ['FD', '1', 'DiffusionBValueYZ'], + '0x9607': ['FD', '1', 'DiffusionBValueZZ'], + '0x9701': ['DT', '1', 'DecayCorrectionDateTime'], + '0x9715': ['FD', '1', 'StartDensityThreshold'], + '0x9716': ['FD', '1', 'StartRelativeDensityDifferenceThreshold'], + '0x9717': ['FD', '1', 'StartCardiacTriggerCountThreshold'], + '0x9718': ['FD', '1', 'StartRespiratoryTriggerCountThreshold'], + '0x9719': ['FD', '1', 'TerminationCountsThreshold'], + '0x9720': ['FD', '1', 'TerminationDensityThreshold'], + '0x9721': ['FD', '1', 'TerminationRelativeDensityThreshold'], + '0x9722': ['FD', '1', 'TerminationTimeThreshold'], + '0x9723': ['FD', '1', 'TerminationCardiacTriggerCountThreshold'], + '0x9724': ['FD', '1', 'TerminationRespiratoryTriggerCountThreshold'], + '0x9725': ['CS', '1', 'DetectorGeometry'], + '0x9726': ['FD', '1', 'TransverseDetectorSeparation'], + '0x9727': ['FD', '1', 'AxialDetectorDimension'], + '0x9729': ['US', '1', 'RadiopharmaceuticalAgentNumber'], + '0x9732': ['SQ', '1', 'PETFrameAcquisitionSequence'], + '0x9733': ['SQ', '1', 'PETDetectorMotionDetailsSequence'], + '0x9734': ['SQ', '1', 'PETTableDynamicsSequence'], + '0x9735': ['SQ', '1', 'PETPositionSequence'], + '0x9736': ['SQ', '1', 'PETFrameCorrectionFactorsSequence'], + '0x9737': ['SQ', '1', 'RadiopharmaceuticalUsageSequence'], + '0x9738': ['CS', '1', 'AttenuationCorrectionSource'], + '0x9739': ['US', '1', 'NumberOfIterations'], + '0x9740': ['US', '1', 'NumberOfSubsets'], + '0x9749': ['SQ', '1', 'PETReconstructionSequence'], + '0x9751': ['SQ', '1', 'PETFrameTypeSequence'], + '0x9755': ['CS', '1', 'TimeOfFlightInformationUsed'], + '0x9756': ['CS', '1', 'ReconstructionType'], + '0x9758': ['CS', '1', 'DecayCorrected'], + '0x9759': ['CS', '1', 'AttenuationCorrected'], + '0x9760': ['CS', '1', 'ScatterCorrected'], + '0x9761': ['CS', '1', 'DeadTimeCorrected'], + '0x9762': ['CS', '1', 'GantryMotionCorrected'], + '0x9763': ['CS', '1', 'PatientMotionCorrected'], + '0x9764': ['CS', '1', 'CountLossNormalizationCorrected'], + '0x9765': ['CS', '1', 'RandomsCorrected'], + '0x9766': ['CS', '1', 'NonUniformRadialSamplingCorrected'], + '0x9767': ['CS', '1', 'SensitivityCalibrated'], + '0x9768': ['CS', '1', 'DetectorNormalizationCorrection'], + '0x9769': ['CS', '1', 'IterativeReconstructionMethod'], + '0x9770': ['CS', '1', 'AttenuationCorrectionTemporalRelationship'], + '0x9771': ['SQ', '1', 'PatientPhysiologicalStateSequence'], + '0x9772': ['SQ', '1', 'PatientPhysiologicalStateCodeSequence'], + '0x9801': ['FD', '1-n', 'DepthsOfFocus'], + '0x9803': ['SQ', '1', 'ExcludedIntervalsSequence'], + '0x9804': ['DT', '1', 'ExclusionStartDateTime'], + '0x9805': ['FD', '1', 'ExclusionDuration'], + '0x9806': ['SQ', '1', 'USImageDescriptionSequence'], + '0x9807': ['SQ', '1', 'ImageDataTypeSequence'], + '0x9808': ['CS', '1', 'DataType'], + '0x9809': ['SQ', '1', 'TransducerScanPatternCodeSequence'], + '0x980B': ['CS', '1', 'AliasedDataType'], + '0x980C': ['CS', '1', 'PositionMeasuringDeviceUsed'], + '0x980D': ['SQ', '1', 'TransducerGeometryCodeSequence'], + '0x980E': ['SQ', '1', 'TransducerBeamSteeringCodeSequence'], + '0x980F': ['SQ', '1', 'TransducerApplicationCodeSequence'], + '0x9810': ['xs', '1', 'ZeroVelocityPixelValue'], + '0xA001': ['SQ', '1', 'ContributingEquipmentSequence'], + '0xA002': ['DT', '1', 'ContributionDateTime'], + '0xA003': ['ST', '1', 'ContributionDescription'] + }, + '0x0020': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x000D': ['UI', '1', 'StudyInstanceUID'], + '0x000E': ['UI', '1', 'SeriesInstanceUID'], + '0x0010': ['SH', '1', 'StudyID'], + '0x0011': ['IS', '1', 'SeriesNumber'], + '0x0012': ['IS', '1', 'AcquisitionNumber'], + '0x0013': ['IS', '1', 'InstanceNumber'], + '0x0014': ['IS', '1', 'IsotopeNumber'], + '0x0015': ['IS', '1', 'PhaseNumber'], + '0x0016': ['IS', '1', 'IntervalNumber'], + '0x0017': ['IS', '1', 'TimeSlotNumber'], + '0x0018': ['IS', '1', 'AngleNumber'], + '0x0019': ['IS', '1', 'ItemNumber'], + '0x0020': ['CS', '2', 'PatientOrientation'], + '0x0022': ['IS', '1', 'OverlayNumber'], + '0x0024': ['IS', '1', 'CurveNumber'], + '0x0026': ['IS', '1', 'LUTNumber'], + '0x0030': ['DS', '3', 'ImagePosition'], + '0x0032': ['DS', '3', 'ImagePositionPatient'], + '0x0035': ['DS', '6', 'ImageOrientation'], + '0x0037': ['DS', '6', 'ImageOrientationPatient'], + '0x0050': ['DS', '1', 'Location'], + '0x0052': ['UI', '1', 'FrameOfReferenceUID'], + '0x0060': ['CS', '1', 'Laterality'], + '0x0062': ['CS', '1', 'ImageLaterality'], + '0x0070': ['LO', '1', 'ImageGeometryType'], + '0x0080': ['CS', '1-n', 'MaskingImage'], + '0x00AA': ['IS', '1', 'ReportNumber'], + '0x0100': ['IS', '1', 'TemporalPositionIdentifier'], + '0x0105': ['IS', '1', 'NumberOfTemporalPositions'], + '0x0110': ['DS', '1', 'TemporalResolution'], + '0x0200': ['UI', '1', 'SynchronizationFrameOfReferenceUID'], + '0x0242': ['UI', '1', 'SOPInstanceUIDOfConcatenationSource'], + '0x1000': ['IS', '1', 'SeriesInStudy'], + '0x1001': ['IS', '1', 'AcquisitionsInSeries'], + '0x1002': ['IS', '1', 'ImagesInAcquisition'], + '0x1003': ['IS', '1', 'ImagesInSeries'], + '0x1004': ['IS', '1', 'AcquisitionsInStudy'], + '0x1005': ['IS', '1', 'ImagesInStudy'], + '0x1020': ['LO', '1-n', 'Reference'], + '0x1040': ['LO', '1', 'PositionReferenceIndicator'], + '0x1041': ['DS', '1', 'SliceLocation'], + '0x1070': ['IS', '1-n', 'OtherStudyNumbers'], + '0x1200': ['IS', '1', 'NumberOfPatientRelatedStudies'], + '0x1202': ['IS', '1', 'NumberOfPatientRelatedSeries'], + '0x1204': ['IS', '1', 'NumberOfPatientRelatedInstances'], + '0x1206': ['IS', '1', 'NumberOfStudyRelatedSeries'], + '0x1208': ['IS', '1', 'NumberOfStudyRelatedInstances'], + '0x1209': ['IS', '1', 'NumberOfSeriesRelatedInstances'], + '0x3100': ['CS', '1-n', 'SourceImageIDs'], + '0x3401': ['CS', '1', 'ModifyingDeviceID'], + '0x3402': ['CS', '1', 'ModifiedImageID'], + '0x3403': ['DA', '1', 'ModifiedImageDate'], + '0x3404': ['LO', '1', 'ModifyingDeviceManufacturer'], + '0x3405': ['TM', '1', 'ModifiedImageTime'], + '0x3406': ['LO', '1', 'ModifiedImageDescription'], + '0x4000': ['LT', '1', 'ImageComments'], + '0x5000': ['AT', '1-n', 'OriginalImageIdentification'], + '0x5002': ['LO', '1-n', 'OriginalImageIdentificationNomenclature'], + '0x9056': ['SH', '1', 'StackID'], + '0x9057': ['UL', '1', 'InStackPositionNumber'], + '0x9071': ['SQ', '1', 'FrameAnatomySequence'], + '0x9072': ['CS', '1', 'FrameLaterality'], + '0x9111': ['SQ', '1', 'FrameContentSequence'], + '0x9113': ['SQ', '1', 'PlanePositionSequence'], + '0x9116': ['SQ', '1', 'PlaneOrientationSequence'], + '0x9128': ['UL', '1', 'TemporalPositionIndex'], + '0x9153': ['FD', '1', 'NominalCardiacTriggerDelayTime'], + '0x9154': ['FL', '1', 'NominalCardiacTriggerTimePriorToRPeak'], + '0x9155': ['FL', '1', 'ActualCardiacTriggerTimePriorToRPeak'], + '0x9156': ['US', '1', 'FrameAcquisitionNumber'], + '0x9157': ['UL', '1-n', 'DimensionIndexValues'], + '0x9158': ['LT', '1', 'FrameComments'], + '0x9161': ['UI', '1', 'ConcatenationUID'], + '0x9162': ['US', '1', 'InConcatenationNumber'], + '0x9163': ['US', '1', 'InConcatenationTotalNumber'], + '0x9164': ['UI', '1', 'DimensionOrganizationUID'], + '0x9165': ['AT', '1', 'DimensionIndexPointer'], + '0x9167': ['AT', '1', 'FunctionalGroupPointer'], + '0x9170': ['SQ', '1', 'UnassignedSharedConvertedAttributesSequence'], + '0x9171': ['SQ', '1', 'UnassignedPerFrameConvertedAttributesSequence'], + '0x9172': ['SQ', '1', 'ConversionSourceAttributesSequence'], + '0x9213': ['LO', '1', 'DimensionIndexPrivateCreator'], + '0x9221': ['SQ', '1', 'DimensionOrganizationSequence'], + '0x9222': ['SQ', '1', 'DimensionIndexSequence'], + '0x9228': ['UL', '1', 'ConcatenationFrameOffsetNumber'], + '0x9238': ['LO', '1', 'FunctionalGroupPrivateCreator'], + '0x9241': ['FL', '1', 'NominalPercentageOfCardiacPhase'], + '0x9245': ['FL', '1', 'NominalPercentageOfRespiratoryPhase'], + '0x9246': ['FL', '1', 'StartingRespiratoryAmplitude'], + '0x9247': ['CS', '1', 'StartingRespiratoryPhase'], + '0x9248': ['FL', '1', 'EndingRespiratoryAmplitude'], + '0x9249': ['CS', '1', 'EndingRespiratoryPhase'], + '0x9250': ['CS', '1', 'RespiratoryTriggerType'], + '0x9251': ['FD', '1', 'RRIntervalTimeNominal'], + '0x9252': ['FD', '1', 'ActualCardiacTriggerDelayTime'], + '0x9253': ['SQ', '1', 'RespiratorySynchronizationSequence'], + '0x9254': ['FD', '1', 'RespiratoryIntervalTime'], + '0x9255': ['FD', '1', 'NominalRespiratoryTriggerDelayTime'], + '0x9256': ['FD', '1', 'RespiratoryTriggerDelayThreshold'], + '0x9257': ['FD', '1', 'ActualRespiratoryTriggerDelayTime'], + '0x9301': ['FD', '3', 'ImagePositionVolume'], + '0x9302': ['FD', '6', 'ImageOrientationVolume'], + '0x9307': ['CS', '1', 'UltrasoundAcquisitionGeometry'], + '0x9308': ['FD', '3', 'ApexPosition'], + '0x9309': ['FD', '16', 'VolumeToTransducerMappingMatrix'], + '0x930A': ['FD', '16', 'VolumeToTableMappingMatrix'], + '0x930B': ['CS', '1', 'VolumeToTransducerRelationship'], + '0x930C': ['CS', '1', 'PatientFrameOfReferenceSource'], + '0x930D': ['FD', '1', 'TemporalPositionTimeOffset'], + '0x930E': ['SQ', '1', 'PlanePositionVolumeSequence'], + '0x930F': ['SQ', '1', 'PlaneOrientationVolumeSequence'], + '0x9310': ['SQ', '1', 'TemporalPositionSequence'], + '0x9311': ['CS', '1', 'DimensionOrganizationType'], + '0x9312': ['UI', '1', 'VolumeFrameOfReferenceUID'], + '0x9313': ['UI', '1', 'TableFrameOfReferenceUID'], + '0x9421': ['LO', '1', 'DimensionDescriptionLabel'], + '0x9450': ['SQ', '1', 'PatientOrientationInFrameSequence'], + '0x9453': ['LO', '1', 'FrameLabel'], + '0x9518': ['US', '1-n', 'AcquisitionIndex'], + '0x9529': ['SQ', '1', 'ContributingSOPInstancesReferenceSequence'], + '0x9536': ['US', '1', 'ReconstructionIndex'] + }, + '0x0022': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['US', '1', 'LightPathFilterPassThroughWavelength'], + '0x0002': ['US', '2', 'LightPathFilterPassBand'], + '0x0003': ['US', '1', 'ImagePathFilterPassThroughWavelength'], + '0x0004': ['US', '2', 'ImagePathFilterPassBand'], + '0x0005': ['CS', '1', 'PatientEyeMovementCommanded'], + '0x0006': ['SQ', '1', 'PatientEyeMovementCommandCodeSequence'], + '0x0007': ['FL', '1', 'SphericalLensPower'], + '0x0008': ['FL', '1', 'CylinderLensPower'], + '0x0009': ['FL', '1', 'CylinderAxis'], + '0x000A': ['FL', '1', 'EmmetropicMagnification'], + '0x000B': ['FL', '1', 'IntraOcularPressure'], + '0x000C': ['FL', '1', 'HorizontalFieldOfView'], + '0x000D': ['CS', '1', 'PupilDilated'], + '0x000E': ['FL', '1', 'DegreeOfDilation'], + '0x0010': ['FL', '1', 'StereoBaselineAngle'], + '0x0011': ['FL', '1', 'StereoBaselineDisplacement'], + '0x0012': ['FL', '1', 'StereoHorizontalPixelOffset'], + '0x0013': ['FL', '1', 'StereoVerticalPixelOffset'], + '0x0014': ['FL', '1', 'StereoRotation'], + '0x0015': ['SQ', '1', 'AcquisitionDeviceTypeCodeSequence'], + '0x0016': ['SQ', '1', 'IlluminationTypeCodeSequence'], + '0x0017': ['SQ', '1', 'LightPathFilterTypeStackCodeSequence'], + '0x0018': ['SQ', '1', 'ImagePathFilterTypeStackCodeSequence'], + '0x0019': ['SQ', '1', 'LensesCodeSequence'], + '0x001A': ['SQ', '1', 'ChannelDescriptionCodeSequence'], + '0x001B': ['SQ', '1', 'RefractiveStateSequence'], + '0x001C': ['SQ', '1', 'MydriaticAgentCodeSequence'], + '0x001D': ['SQ', '1', 'RelativeImagePositionCodeSequence'], + '0x001E': ['FL', '1', 'CameraAngleOfView'], + '0x0020': ['SQ', '1', 'StereoPairsSequence'], + '0x0021': ['SQ', '1', 'LeftImageSequence'], + '0x0022': ['SQ', '1', 'RightImageSequence'], + '0x0028': ['CS', '1', 'StereoPairsPresent'], + '0x0030': ['FL', '1', 'AxialLengthOfTheEye'], + '0x0031': ['SQ', '1', 'OphthalmicFrameLocationSequence'], + '0x0032': ['FL', '2-2n', 'ReferenceCoordinates'], + '0x0035': ['FL', '1', 'DepthSpatialResolution'], + '0x0036': ['FL', '1', 'MaximumDepthDistortion'], + '0x0037': ['FL', '1', 'AlongScanSpatialResolution'], + '0x0038': ['FL', '1', 'MaximumAlongScanDistortion'], + '0x0039': ['CS', '1', 'OphthalmicImageOrientation'], + '0x0041': ['FL', '1', 'DepthOfTransverseImage'], + '0x0042': ['SQ', '1', 'MydriaticAgentConcentrationUnitsSequence'], + '0x0048': ['FL', '1', 'AcrossScanSpatialResolution'], + '0x0049': ['FL', '1', 'MaximumAcrossScanDistortion'], + '0x004E': ['DS', '1', 'MydriaticAgentConcentration'], + '0x0055': ['FL', '1', 'IlluminationWaveLength'], + '0x0056': ['FL', '1', 'IlluminationPower'], + '0x0057': ['FL', '1', 'IlluminationBandwidth'], + '0x0058': ['SQ', '1', 'MydriaticAgentSequence'], + '0x1007': ['SQ', '1', 'OphthalmicAxialMeasurementsRightEyeSequence'], + '0x1008': ['SQ', '1', 'OphthalmicAxialMeasurementsLeftEyeSequence'], + '0x1009': ['CS', '1', 'OphthalmicAxialMeasurementsDeviceType'], + '0x1010': ['CS', '1', 'OphthalmicAxialLengthMeasurementsType'], + '0x1012': ['SQ', '1', 'OphthalmicAxialLengthSequence'], + '0x1019': ['FL', '1', 'OphthalmicAxialLength'], + '0x1024': ['SQ', '1', 'LensStatusCodeSequence'], + '0x1025': ['SQ', '1', 'VitreousStatusCodeSequence'], + '0x1028': ['SQ', '1', 'IOLFormulaCodeSequence'], + '0x1029': ['LO', '1', 'IOLFormulaDetail'], + '0x1033': ['FL', '1', 'KeratometerIndex'], + '0x1035': ['SQ', '1', 'SourceOfOphthalmicAxialLengthCodeSequence'], + '0x1037': ['FL', '1', 'TargetRefraction'], + '0x1039': ['CS', '1', 'RefractiveProcedureOccurred'], + '0x1040': ['SQ', '1', 'RefractiveSurgeryTypeCodeSequence'], + '0x1044': ['SQ', '1', 'OphthalmicUltrasoundMethodCodeSequence'], + '0x1050': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSequence'], + '0x1053': ['FL', '1', 'IOLPower'], + '0x1054': ['FL', '1', 'PredictedRefractiveError'], + '0x1059': ['FL', '1', 'OphthalmicAxialLengthVelocity'], + '0x1065': ['LO', '1', 'LensStatusDescription'], + '0x1066': ['LO', '1', 'VitreousStatusDescription'], + '0x1090': ['SQ', '1', 'IOLPowerSequence'], + '0x1092': ['SQ', '1', 'LensConstantSequence'], + '0x1093': ['LO', '1', 'IOLManufacturer'], + '0x1094': ['LO', '1', 'LensConstantDescription'], + '0x1095': ['LO', '1', 'ImplantName'], + '0x1096': ['SQ', '1', 'KeratometryMeasurementTypeCodeSequence'], + '0x1097': ['LO', '1', 'ImplantPartNumber'], + '0x1100': ['SQ', '1', 'ReferencedOphthalmicAxialMeasurementsSequence'], + '0x1101': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSegmentNameCodeSequence'], + '0x1103': ['SQ', '1', 'RefractiveErrorBeforeRefractiveSurgeryCodeSequence'], + '0x1121': ['FL', '1', 'IOLPowerForExactEmmetropia'], + '0x1122': ['FL', '1', 'IOLPowerForExactTargetRefraction'], + '0x1125': ['SQ', '1', 'AnteriorChamberDepthDefinitionCodeSequence'], + '0x1127': ['SQ', '1', 'LensThicknessSequence'], + '0x1128': ['SQ', '1', 'AnteriorChamberDepthSequence'], + '0x1130': ['FL', '1', 'LensThickness'], + '0x1131': ['FL', '1', 'AnteriorChamberDepth'], + '0x1132': ['SQ', '1', 'SourceOfLensThicknessDataCodeSequence'], + '0x1133': ['SQ', '1', 'SourceOfAnteriorChamberDepthDataCodeSequence'], + '0x1134': ['SQ', '1', 'SourceOfRefractiveMeasurementsSequence'], + '0x1135': ['SQ', '1', 'SourceOfRefractiveMeasurementsCodeSequence'], + '0x1140': ['CS', '1', 'OphthalmicAxialLengthMeasurementModified'], + '0x1150': ['SQ', '1', 'OphthalmicAxialLengthDataSourceCodeSequence'], + '0x1153': ['SQ', '1', 'OphthalmicAxialLengthAcquisitionMethodCodeSequence'], + '0x1155': ['FL', '1', 'SignalToNoiseRatio'], + '0x1159': ['LO', '1', 'OphthalmicAxialLengthDataSourceDescription'], + '0x1210': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsTotalLengthSequence'], + '0x1211': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSegmentalLengthSequence'], + '0x1212': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsLengthSummationSequence'], + '0x1220': ['SQ', '1', 'UltrasoundOphthalmicAxialLengthMeasurementsSequence'], + '0x1225': ['SQ', '1', 'OpticalOphthalmicAxialLengthMeasurementsSequence'], + '0x1230': ['SQ', '1', 'UltrasoundSelectedOphthalmicAxialLengthSequence'], + '0x1250': ['SQ', '1', 'OphthalmicAxialLengthSelectionMethodCodeSequence'], + '0x1255': ['SQ', '1', 'OpticalSelectedOphthalmicAxialLengthSequence'], + '0x1257': ['SQ', '1', 'SelectedSegmentalOphthalmicAxialLengthSequence'], + '0x1260': ['SQ', '1', 'SelectedTotalOphthalmicAxialLengthSequence'], + '0x1262': ['SQ', '1', 'OphthalmicAxialLengthQualityMetricSequence'], + '0x1265': ['SQ', '1', 'OphthalmicAxialLengthQualityMetricTypeCodeSequence'], + '0x1273': ['LO', '1', 'OphthalmicAxialLengthQualityMetricTypeDescription'], + '0x1300': ['SQ', '1', 'IntraocularLensCalculationsRightEyeSequence'], + '0x1310': ['SQ', '1', 'IntraocularLensCalculationsLeftEyeSequence'], + '0x1330': ['SQ', '1', 'ReferencedOphthalmicAxialLengthMeasurementQCImageSequence'], + '0x1415': ['CS', '1', 'OphthalmicMappingDeviceType'], + '0x1420': ['SQ', '1', 'AcquisitionMethodCodeSequence'], + '0x1423': ['SQ', '1', 'AcquisitionMethodAlgorithmSequence'], + '0x1436': ['SQ', '1', 'OphthalmicThicknessMapTypeCodeSequence'], + '0x1443': ['SQ', '1', 'OphthalmicThicknessMappingNormalsSequence'], + '0x1445': ['SQ', '1', 'RetinalThicknessDefinitionCodeSequence'], + '0x1450': ['SQ', '1', 'PixelValueMappingToCodedConceptSequence'], + '0x1452': ['xs', '1', 'MappedPixelValue'], + '0x1454': ['LO', '1', 'PixelValueMappingExplanation'], + '0x1458': ['SQ', '1', 'OphthalmicThicknessMapQualityThresholdSequence'], + '0x1460': ['FL', '1', 'OphthalmicThicknessMapThresholdQualityRating'], + '0x1463': ['FL', '2', 'AnatomicStructureReferencePoint'], + '0x1465': ['SQ', '1', 'RegistrationToLocalizerSequence'], + '0x1466': ['CS', '1', 'RegisteredLocalizerUnits'], + '0x1467': ['FL', '2', 'RegisteredLocalizerTopLeftHandCorner'], + '0x1468': ['FL', '2', 'RegisteredLocalizerBottomRightHandCorner'], + '0x1470': ['SQ', '1', 'OphthalmicThicknessMapQualityRatingSequence'], + '0x1472': ['SQ', '1', 'RelevantOPTAttributesSequence'], + '0x1512': ['SQ', '1', 'TransformationMethodCodeSequence'], + '0x1513': ['SQ', '1', 'TransformationAlgorithmSequence'], + '0x1515': ['CS', '1', 'OphthalmicAxialLengthMethod'], + '0x1517': ['FL', '1', 'OphthalmicFOV'], + '0x1518': ['SQ', '1', 'TwoDimensionalToThreeDimensionalMapSequence'], + '0x1525': ['SQ', '1', 'WideFieldOphthalmicPhotographyQualityRatingSequence'], + '0x1526': ['SQ', '1', 'WideFieldOphthalmicPhotographyQualityThresholdSequence'], + '0x1527': ['FL', '1', 'WideFieldOphthalmicPhotographyThresholdQualityRating'], + '0x1528': ['FL', '1', 'XCoordinatesCenterPixelViewAngle'], + '0x1529': ['FL', '1', 'YCoordinatesCenterPixelViewAngle'], + '0x1530': ['UL', '1', 'NumberOfMapPoints'], + '0x1531': ['OF', '1', 'TwoDimensionalToThreeDimensionalMapData'] + }, + '0x0024': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['FL', '1', 'VisualFieldHorizontalExtent'], + '0x0011': ['FL', '1', 'VisualFieldVerticalExtent'], + '0x0012': ['CS', '1', 'VisualFieldShape'], + '0x0016': ['SQ', '1', 'ScreeningTestModeCodeSequence'], + '0x0018': ['FL', '1', 'MaximumStimulusLuminance'], + '0x0020': ['FL', '1', 'BackgroundLuminance'], + '0x0021': ['SQ', '1', 'StimulusColorCodeSequence'], + '0x0024': ['SQ', '1', 'BackgroundIlluminationColorCodeSequence'], + '0x0025': ['FL', '1', 'StimulusArea'], + '0x0028': ['FL', '1', 'StimulusPresentationTime'], + '0x0032': ['SQ', '1', 'FixationSequence'], + '0x0033': ['SQ', '1', 'FixationMonitoringCodeSequence'], + '0x0034': ['SQ', '1', 'VisualFieldCatchTrialSequence'], + '0x0035': ['US', '1', 'FixationCheckedQuantity'], + '0x0036': ['US', '1', 'PatientNotProperlyFixatedQuantity'], + '0x0037': ['CS', '1', 'PresentedVisualStimuliDataFlag'], + '0x0038': ['US', '1', 'NumberOfVisualStimuli'], + '0x0039': ['CS', '1', 'ExcessiveFixationLossesDataFlag'], + '0x0040': ['CS', '1', 'ExcessiveFixationLosses'], + '0x0042': ['US', '1', 'StimuliRetestingQuantity'], + '0x0044': ['LT', '1', 'CommentsOnPatientPerformanceOfVisualField'], + '0x0045': ['CS', '1', 'FalseNegativesEstimateFlag'], + '0x0046': ['FL', '1', 'FalseNegativesEstimate'], + '0x0048': ['US', '1', 'NegativeCatchTrialsQuantity'], + '0x0050': ['US', '1', 'FalseNegativesQuantity'], + '0x0051': ['CS', '1', 'ExcessiveFalseNegativesDataFlag'], + '0x0052': ['CS', '1', 'ExcessiveFalseNegatives'], + '0x0053': ['CS', '1', 'FalsePositivesEstimateFlag'], + '0x0054': ['FL', '1', 'FalsePositivesEstimate'], + '0x0055': ['CS', '1', 'CatchTrialsDataFlag'], + '0x0056': ['US', '1', 'PositiveCatchTrialsQuantity'], + '0x0057': ['CS', '1', 'TestPointNormalsDataFlag'], + '0x0058': ['SQ', '1', 'TestPointNormalsSequence'], + '0x0059': ['CS', '1', 'GlobalDeviationProbabilityNormalsFlag'], + '0x0060': ['US', '1', 'FalsePositivesQuantity'], + '0x0061': ['CS', '1', 'ExcessiveFalsePositivesDataFlag'], + '0x0062': ['CS', '1', 'ExcessiveFalsePositives'], + '0x0063': ['CS', '1', 'VisualFieldTestNormalsFlag'], + '0x0064': ['SQ', '1', 'ResultsNormalsSequence'], + '0x0065': ['SQ', '1', 'AgeCorrectedSensitivityDeviationAlgorithmSequence'], + '0x0066': ['FL', '1', 'GlobalDeviationFromNormal'], + '0x0067': ['SQ', '1', 'GeneralizedDefectSensitivityDeviationAlgorithmSequence'], + '0x0068': ['FL', '1', 'LocalizedDeviationFromNormal'], + '0x0069': ['LO', '1', 'PatientReliabilityIndicator'], + '0x0070': ['FL', '1', 'VisualFieldMeanSensitivity'], + '0x0071': ['FL', '1', 'GlobalDeviationProbability'], + '0x0072': ['CS', '1', 'LocalDeviationProbabilityNormalsFlag'], + '0x0073': ['FL', '1', 'LocalizedDeviationProbability'], + '0x0074': ['CS', '1', 'ShortTermFluctuationCalculated'], + '0x0075': ['FL', '1', 'ShortTermFluctuation'], + '0x0076': ['CS', '1', 'ShortTermFluctuationProbabilityCalculated'], + '0x0077': ['FL', '1', 'ShortTermFluctuationProbability'], + '0x0078': ['CS', '1', 'CorrectedLocalizedDeviationFromNormalCalculated'], + '0x0079': ['FL', '1', 'CorrectedLocalizedDeviationFromNormal'], + '0x0080': ['CS', '1', 'CorrectedLocalizedDeviationFromNormalProbabilityCalculated'], + '0x0081': ['FL', '1', 'CorrectedLocalizedDeviationFromNormalProbability'], + '0x0083': ['SQ', '1', 'GlobalDeviationProbabilitySequence'], + '0x0085': ['SQ', '1', 'LocalizedDeviationProbabilitySequence'], + '0x0086': ['CS', '1', 'FovealSensitivityMeasured'], + '0x0087': ['FL', '1', 'FovealSensitivity'], + '0x0088': ['FL', '1', 'VisualFieldTestDuration'], + '0x0089': ['SQ', '1', 'VisualFieldTestPointSequence'], + '0x0090': ['FL', '1', 'VisualFieldTestPointXCoordinate'], + '0x0091': ['FL', '1', 'VisualFieldTestPointYCoordinate'], + '0x0092': ['FL', '1', 'AgeCorrectedSensitivityDeviationValue'], + '0x0093': ['CS', '1', 'StimulusResults'], + '0x0094': ['FL', '1', 'SensitivityValue'], + '0x0095': ['CS', '1', 'RetestStimulusSeen'], + '0x0096': ['FL', '1', 'RetestSensitivityValue'], + '0x0097': ['SQ', '1', 'VisualFieldTestPointNormalsSequence'], + '0x0098': ['FL', '1', 'QuantifiedDefect'], + '0x0100': ['FL', '1', 'AgeCorrectedSensitivityDeviationProbabilityValue'], + '0x0102': ['CS', '1', 'GeneralizedDefectCorrectedSensitivityDeviationFlag'], + '0x0103': ['FL', '1', 'GeneralizedDefectCorrectedSensitivityDeviationValue'], + '0x0104': ['FL', '1', 'GeneralizedDefectCorrectedSensitivityDeviationProbabilityValue'], + '0x0105': ['FL', '1', 'MinimumSensitivityValue'], + '0x0106': ['CS', '1', 'BlindSpotLocalized'], + '0x0107': ['FL', '1', 'BlindSpotXCoordinate'], + '0x0108': ['FL', '1', 'BlindSpotYCoordinate'], + '0x0110': ['SQ', '1', 'VisualAcuityMeasurementSequence'], + '0x0112': ['SQ', '1', 'RefractiveParametersUsedOnPatientSequence'], + '0x0113': ['CS', '1', 'MeasurementLaterality'], + '0x0114': ['SQ', '1', 'OphthalmicPatientClinicalInformationLeftEyeSequence'], + '0x0115': ['SQ', '1', 'OphthalmicPatientClinicalInformationRightEyeSequence'], + '0x0117': ['CS', '1', 'FovealPointNormativeDataFlag'], + '0x0118': ['FL', '1', 'FovealPointProbabilityValue'], + '0x0120': ['CS', '1', 'ScreeningBaselineMeasured'], + '0x0122': ['SQ', '1', 'ScreeningBaselineMeasuredSequence'], + '0x0124': ['CS', '1', 'ScreeningBaselineType'], + '0x0126': ['FL', '1', 'ScreeningBaselineValue'], + '0x0202': ['LO', '1', 'AlgorithmSource'], + '0x0306': ['LO', '1', 'DataSetName'], + '0x0307': ['LO', '1', 'DataSetVersion'], + '0x0308': ['LO', '1', 'DataSetSource'], + '0x0309': ['LO', '1', 'DataSetDescription'], + '0x0317': ['SQ', '1', 'VisualFieldTestReliabilityGlobalIndexSequence'], + '0x0320': ['SQ', '1', 'VisualFieldGlobalResultsIndexSequence'], + '0x0325': ['SQ', '1', 'DataObservationSequence'], + '0x0338': ['CS', '1', 'IndexNormalsFlag'], + '0x0341': ['FL', '1', 'IndexProbability'], + '0x0344': ['SQ', '1', 'IndexProbabilitySequence'] + }, + '0x0028': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['US', '1', 'SamplesPerPixel'], + '0x0003': ['US', '1', 'SamplesPerPixelUsed'], + '0x0004': ['CS', '1', 'PhotometricInterpretation'], + '0x0005': ['US', '1', 'ImageDimensions'], + '0x0006': ['US', '1', 'PlanarConfiguration'], + '0x0008': ['IS', '1', 'NumberOfFrames'], + '0x0009': ['AT', '1-n', 'FrameIncrementPointer'], + '0x000A': ['AT', '1-n', 'FrameDimensionPointer'], + '0x0010': ['US', '1', 'Rows'], + '0x0011': ['US', '1', 'Columns'], + '0x0012': ['US', '1', 'Planes'], + '0x0014': ['US', '1', 'UltrasoundColorDataPresent'], + '0x0020': ['', '', ''], + '0x0030': ['DS', '2', 'PixelSpacing'], + '0x0031': ['DS', '2', 'ZoomFactor'], + '0x0032': ['DS', '2', 'ZoomCenter'], + '0x0034': ['IS', '2', 'PixelAspectRatio'], + '0x0040': ['CS', '1', 'ImageFormat'], + '0x0050': ['LO', '1-n', 'ManipulatedImage'], + '0x0051': ['CS', '1-n', 'CorrectedImage'], + '0x005F': ['LO', '1', 'CompressionRecognitionCode'], + '0x0060': ['CS', '1', 'CompressionCode'], + '0x0061': ['SH', '1', 'CompressionOriginator'], + '0x0062': ['LO', '1', 'CompressionLabel'], + '0x0063': ['SH', '1', 'CompressionDescription'], + '0x0065': ['CS', '1-n', 'CompressionSequence'], + '0x0066': ['AT', '1-n', 'CompressionStepPointers'], + '0x0068': ['US', '1', 'RepeatInterval'], + '0x0069': ['US', '1', 'BitsGrouped'], + '0x0070': ['US', '1-n', 'PerimeterTable'], + '0x0071': ['xs', '1', 'PerimeterValue'], + '0x0080': ['US', '1', 'PredictorRows'], + '0x0081': ['US', '1', 'PredictorColumns'], + '0x0082': ['US', '1-n', 'PredictorConstants'], + '0x0090': ['CS', '1', 'BlockedPixels'], + '0x0091': ['US', '1', 'BlockRows'], + '0x0092': ['US', '1', 'BlockColumns'], + '0x0093': ['US', '1', 'RowOverlap'], + '0x0094': ['US', '1', 'ColumnOverlap'], + '0x0100': ['US', '1', 'BitsAllocated'], + '0x0101': ['US', '1', 'BitsStored'], + '0x0102': ['US', '1', 'HighBit'], + '0x0103': ['US', '1', 'PixelRepresentation'], + '0x0104': ['xs', '1', 'SmallestValidPixelValue'], + '0x0105': ['xs', '1', 'LargestValidPixelValue'], + '0x0106': ['xs', '1', 'SmallestImagePixelValue'], + '0x0107': ['xs', '1', 'LargestImagePixelValue'], + '0x0108': ['xs', '1', 'SmallestPixelValueInSeries'], + '0x0109': ['xs', '1', 'LargestPixelValueInSeries'], + '0x0110': ['xs', '1', 'SmallestImagePixelValueInPlane'], + '0x0111': ['xs', '1', 'LargestImagePixelValueInPlane'], + '0x0120': ['xs', '1', 'PixelPaddingValue'], + '0x0121': ['xs', '1', 'PixelPaddingRangeLimit'], + '0x0122': ['FL', '1', 'FloatPixelPaddingValue'], + '0x0123': ['FD', '1', 'DoubleFloatPixelPaddingValue'], + '0x0124': ['FL', '1', 'FloatPixelPaddingRangeLimit'], + '0x0125': ['FD', '1', 'DoubleFloatPixelPaddingRangeLimit'], + '0x0200': ['US', '1', 'ImageLocation'], + '0x0300': ['CS', '1', 'QualityControlImage'], + '0x0301': ['CS', '1', 'BurnedInAnnotation'], + '0x0302': ['CS', '1', 'RecognizableVisualFeatures'], + '0x0303': ['CS', '1', 'LongitudinalTemporalInformationModified'], + '0x0304': ['UI', '1', 'ReferencedColorPaletteInstanceUID'], + '0x0400': ['LO', '1', 'TransformLabel'], + '0x0401': ['LO', '1', 'TransformVersionNumber'], + '0x0402': ['US', '1', 'NumberOfTransformSteps'], + '0x0403': ['LO', '1-n', 'SequenceOfCompressedData'], + '0x0404': ['AT', '1-n', 'DetailsOfCoefficients'], + '0x04x0': ['US', '1', 'RowsForNthOrderCoefficients'], + '0x04x1': ['US', '1', 'ColumnsForNthOrderCoefficients'], + '0x04x2': ['LO', '1-n', 'CoefficientCoding'], + '0x04x3': ['AT', '1-n', 'CoefficientCodingPointers'], + '0x0700': ['LO', '1', 'DCTLabel'], + '0x0701': ['CS', '1-n', 'DataBlockDescription'], + '0x0702': ['AT', '1-n', 'DataBlock'], + '0x0710': ['US', '1', 'NormalizationFactorFormat'], + '0x0720': ['US', '1', 'ZonalMapNumberFormat'], + '0x0721': ['AT', '1-n', 'ZonalMapLocation'], + '0x0722': ['US', '1', 'ZonalMapFormat'], + '0x0730': ['US', '1', 'AdaptiveMapFormat'], + '0x0740': ['US', '1', 'CodeNumberFormat'], + '0x08x0': ['CS', '1-n', 'CodeLabel'], + '0x08x2': ['US', '1', 'NumberOfTables'], + '0x08x3': ['AT', '1-n', 'CodeTableLocation'], + '0x08x4': ['US', '1', 'BitsForCodeWord'], + '0x08x8': ['AT', '1-n', 'ImageDataLocation'], + '0x0A02': ['CS', '1', 'PixelSpacingCalibrationType'], + '0x0A04': ['LO', '1', 'PixelSpacingCalibrationDescription'], + '0x1040': ['CS', '1', 'PixelIntensityRelationship'], + '0x1041': ['SS', '1', 'PixelIntensityRelationshipSign'], + '0x1050': ['DS', '1-n', 'WindowCenter'], + '0x1051': ['DS', '1-n', 'WindowWidth'], + '0x1052': ['DS', '1', 'RescaleIntercept'], + '0x1053': ['DS', '1', 'RescaleSlope'], + '0x1054': ['LO', '1', 'RescaleType'], + '0x1055': ['LO', '1-n', 'WindowCenterWidthExplanation'], + '0x1056': ['CS', '1', 'VOILUTFunction'], + '0x1080': ['CS', '1', 'GrayScale'], + '0x1090': ['CS', '1', 'RecommendedViewingMode'], + '0x1100': ['xs', '3', 'GrayLookupTableDescriptor'], + '0x1101': ['xs', '3', 'RedPaletteColorLookupTableDescriptor'], + '0x1102': ['xs', '3', 'GreenPaletteColorLookupTableDescriptor'], + '0x1103': ['xs', '3', 'BluePaletteColorLookupTableDescriptor'], + '0x1104': ['US', '3', 'AlphaPaletteColorLookupTableDescriptor'], + '0x1111': ['xs', '4', 'LargeRedPaletteColorLookupTableDescriptor'], + '0x1112': ['xs', '4', 'LargeGreenPaletteColorLookupTableDescriptor'], + '0x1113': ['xs', '4', 'LargeBluePaletteColorLookupTableDescriptor'], + '0x1199': ['UI', '1', 'PaletteColorLookupTableUID'], + '0x1200': ['US or SS or OW', '1-n or 1', 'GrayLookupTableData'], + '0x1201': ['OW', '1', 'RedPaletteColorLookupTableData'], + '0x1202': ['OW', '1', 'GreenPaletteColorLookupTableData'], + '0x1203': ['OW', '1', 'BluePaletteColorLookupTableData'], + '0x1204': ['OW', '1', 'AlphaPaletteColorLookupTableData'], + '0x1211': ['OW', '1', 'LargeRedPaletteColorLookupTableData'], + '0x1212': ['OW', '1', 'LargeGreenPaletteColorLookupTableData'], + '0x1213': ['OW', '1', 'LargeBluePaletteColorLookupTableData'], + '0x1214': ['UI', '1', 'LargePaletteColorLookupTableUID'], + '0x1221': ['OW', '1', 'SegmentedRedPaletteColorLookupTableData'], + '0x1222': ['OW', '1', 'SegmentedGreenPaletteColorLookupTableData'], + '0x1223': ['OW', '1', 'SegmentedBluePaletteColorLookupTableData'], + '0x1300': ['CS', '1', 'BreastImplantPresent'], + '0x1350': ['CS', '1', 'PartialView'], + '0x1351': ['ST', '1', 'PartialViewDescription'], + '0x1352': ['SQ', '1', 'PartialViewCodeSequence'], + '0x135A': ['CS', '1', 'SpatialLocationsPreserved'], + '0x1401': ['SQ', '1', 'DataFrameAssignmentSequence'], + '0x1402': ['CS', '1', 'DataPathAssignment'], + '0x1403': ['US', '1', 'BitsMappedToColorLookupTable'], + '0x1404': ['SQ', '1', 'BlendingLUT1Sequence'], + '0x1405': ['CS', '1', 'BlendingLUT1TransferFunction'], + '0x1406': ['FD', '1', 'BlendingWeightConstant'], + '0x1407': ['US', '3', 'BlendingLookupTableDescriptor'], + '0x1408': ['OW', '1', 'BlendingLookupTableData'], + '0x140B': ['SQ', '1', 'EnhancedPaletteColorLookupTableSequence'], + '0x140C': ['SQ', '1', 'BlendingLUT2Sequence'], + '0x140D': ['CS', '1', 'BlendingLUT2TransferFunction'], + '0x140E': ['CS', '1', 'DataPathID'], + '0x140F': ['CS', '1', 'RGBLUTTransferFunction'], + '0x1410': ['CS', '1', 'AlphaLUTTransferFunction'], + '0x2000': ['OB', '1', 'ICCProfile'], + '0x2110': ['CS', '1', 'LossyImageCompression'], + '0x2112': ['DS', '1-n', 'LossyImageCompressionRatio'], + '0x2114': ['CS', '1-n', 'LossyImageCompressionMethod'], + '0x3000': ['SQ', '1', 'ModalityLUTSequence'], + '0x3002': ['xs', '3', 'LUTDescriptor'], + '0x3003': ['LO', '1', 'LUTExplanation'], + '0x3004': ['LO', '1', 'ModalityLUTType'], + '0x3006': ['US or OW', '1-n or 1', 'LUTData'], + '0x3010': ['SQ', '1', 'VOILUTSequence'], + '0x3110': ['SQ', '1', 'SoftcopyVOILUTSequence'], + '0x4000': ['LT', '1', 'ImagePresentationComments'], + '0x5000': ['SQ', '1', 'BiPlaneAcquisitionSequence'], + '0x6010': ['US', '1', 'RepresentativeFrameNumber'], + '0x6020': ['US', '1-n', 'FrameNumbersOfInterest'], + '0x6022': ['LO', '1-n', 'FrameOfInterestDescription'], + '0x6023': ['CS', '1-n', 'FrameOfInterestType'], + '0x6030': ['US', '1-n', 'MaskPointers'], + '0x6040': ['US', '1-n', 'RWavePointer'], + '0x6100': ['SQ', '1', 'MaskSubtractionSequence'], + '0x6101': ['CS', '1', 'MaskOperation'], + '0x6102': ['US', '2-2n', 'ApplicableFrameRange'], + '0x6110': ['US', '1-n', 'MaskFrameNumbers'], + '0x6112': ['US', '1', 'ContrastFrameAveraging'], + '0x6114': ['FL', '2', 'MaskSubPixelShift'], + '0x6120': ['SS', '1', 'TIDOffset'], + '0x6190': ['ST', '1', 'MaskOperationExplanation'], + '0x7000': ['SQ', '1', 'EquipmentAdministratorSequence'], + '0x7001': ['US', '1', 'NumberOfDisplaySubsystems'], + '0x7002': ['US', '1', 'CurrentConfigurationID'], + '0x7003': ['US', '1', 'DisplaySubsystemID'], + '0x7004': ['SH', '1', 'DisplaySubsystemName'], + '0x7005': ['LO', '1', 'DisplaySubsystemDescription'], + '0x7006': ['CS', '1', 'SystemStatus'], + '0x7007': ['LO', '1', 'SystemStatusComment'], + '0x7008': ['SQ', '1', 'TargetLuminanceCharacteristicsSequence'], + '0x7009': ['US', '1', 'LuminanceCharacteristicsID'], + '0x700A': ['SQ', '1', 'DisplaySubsystemConfigurationSequence'], + '0x700B': ['US', '1', 'ConfigurationID'], + '0x700C': ['SH', '1', 'ConfigurationName'], + '0x700D': ['LO', '1', 'ConfigurationDescription'], + '0x700E': ['US', '1', 'ReferencedTargetLuminanceCharacteristicsID'], + '0x700F': ['SQ', '1', 'QAResultsSequence'], + '0x7010': ['SQ', '1', 'DisplaySubsystemQAResultsSequence'], + '0x7011': ['SQ', '1', 'ConfigurationQAResultsSequence'], + '0x7012': ['SQ', '1', 'MeasurementEquipmentSequence'], + '0x7013': ['CS', '1-n', 'MeasurementFunctions'], + '0x7014': ['CS', '1', 'MeasurementEquipmentType'], + '0x7015': ['SQ', '1', 'VisualEvaluationResultSequence'], + '0x7016': ['SQ', '1', 'DisplayCalibrationResultSequence'], + '0x7017': ['US', '1', 'DDLValue'], + '0x7018': ['FL', '2', 'CIExyWhitePoint'], + '0x7019': ['CS', '1', 'DisplayFunctionType'], + '0x701A': ['FL', '1', 'GammaValue'], + '0x701B': ['US', '1', 'NumberOfLuminancePoints'], + '0x701C': ['SQ', '1', 'LuminanceResponseSequence'], + '0x701D': ['FL', '1', 'TargetMinimumLuminance'], + '0x701E': ['FL', '1', 'TargetMaximumLuminance'], + '0x701F': ['FL', '1', 'LuminanceValue'], + '0x7020': ['LO', '1', 'LuminanceResponseDescription'], + '0x7021': ['CS', '1', 'WhitePointFlag'], + '0x7022': ['SQ', '1', 'DisplayDeviceTypeCodeSequence'], + '0x7023': ['SQ', '1', 'DisplaySubsystemSequence'], + '0x7024': ['SQ', '1', 'LuminanceResultSequence'], + '0x7025': ['CS', '1', 'AmbientLightValueSource'], + '0x7026': ['CS', '1-n', 'MeasuredCharacteristics'], + '0x7027': ['SQ', '1', 'LuminanceUniformityResultSequence'], + '0x7028': ['SQ', '1', 'VisualEvaluationTestSequence'], + '0x7029': ['CS', '1', 'TestResult'], + '0x702A': ['LO', '1', 'TestResultComment'], + '0x702B': ['CS', '1', 'TestImageValidation'], + '0x702C': ['SQ', '1', 'TestPatternCodeSequence'], + '0x702D': ['SQ', '1', 'MeasurementPatternCodeSequence'], + '0x702E': ['SQ', '1', 'VisualEvaluationMethodCodeSequence'], + '0x7FE0': ['UR', '1', 'PixelDataProviderURL'], + '0x9001': ['UL', '1', 'DataPointRows'], + '0x9002': ['UL', '1', 'DataPointColumns'], + '0x9003': ['CS', '1', 'SignalDomainColumns'], + '0x9099': ['US', '1', 'LargestMonochromePixelValue'], + '0x9108': ['CS', '1', 'DataRepresentation'], + '0x9110': ['SQ', '1', 'PixelMeasuresSequence'], + '0x9132': ['SQ', '1', 'FrameVOILUTSequence'], + '0x9145': ['SQ', '1', 'PixelValueTransformationSequence'], + '0x9235': ['CS', '1', 'SignalDomainRows'], + '0x9411': ['FL', '1', 'DisplayFilterPercentage'], + '0x9415': ['SQ', '1', 'FramePixelShiftSequence'], + '0x9416': ['US', '1', 'SubtractionItemID'], + '0x9422': ['SQ', '1', 'PixelIntensityRelationshipLUTSequence'], + '0x9443': ['SQ', '1', 'FramePixelDataPropertiesSequence'], + '0x9444': ['CS', '1', 'GeometricalProperties'], + '0x9445': ['FL', '1', 'GeometricMaximumDistortion'], + '0x9446': ['CS', '1-n', 'ImageProcessingApplied'], + '0x9454': ['CS', '1', 'MaskSelectionMode'], + '0x9474': ['CS', '1', 'LUTFunction'], + '0x9478': ['FL', '1', 'MaskVisibilityPercentage'], + '0x9501': ['SQ', '1', 'PixelShiftSequence'], + '0x9502': ['SQ', '1', 'RegionPixelShiftSequence'], + '0x9503': ['SS', '2-2n', 'VerticesOfTheRegion'], + '0x9505': ['SQ', '1', 'MultiFramePresentationSequence'], + '0x9506': ['US', '2-2n', 'PixelShiftFrameRange'], + '0x9507': ['US', '2-2n', 'LUTFrameRange'], + '0x9520': ['DS', '16', 'ImageToEquipmentMappingMatrix'], + '0x9537': ['CS', '1', 'EquipmentCoordinateSystemIdentification'] + }, + '0x0032': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x000A': ['CS', '1', 'StudyStatusID'], + '0x000C': ['CS', '1', 'StudyPriorityID'], + '0x0012': ['LO', '1', 'StudyIDIssuer'], + '0x0032': ['DA', '1', 'StudyVerifiedDate'], + '0x0033': ['TM', '1', 'StudyVerifiedTime'], + '0x0034': ['DA', '1', 'StudyReadDate'], + '0x0035': ['TM', '1', 'StudyReadTime'], + '0x1000': ['DA', '1', 'ScheduledStudyStartDate'], + '0x1001': ['TM', '1', 'ScheduledStudyStartTime'], + '0x1010': ['DA', '1', 'ScheduledStudyStopDate'], + '0x1011': ['TM', '1', 'ScheduledStudyStopTime'], + '0x1020': ['LO', '1', 'ScheduledStudyLocation'], + '0x1021': ['AE', '1-n', 'ScheduledStudyLocationAETitle'], + '0x1030': ['LO', '1', 'ReasonForStudy'], + '0x1031': ['SQ', '1', 'RequestingPhysicianIdentificationSequence'], + '0x1032': ['PN', '1', 'RequestingPhysician'], + '0x1033': ['LO', '1', 'RequestingService'], + '0x1034': ['SQ', '1', 'RequestingServiceCodeSequence'], + '0x1040': ['DA', '1', 'StudyArrivalDate'], + '0x1041': ['TM', '1', 'StudyArrivalTime'], + '0x1050': ['DA', '1', 'StudyCompletionDate'], + '0x1051': ['TM', '1', 'StudyCompletionTime'], + '0x1055': ['CS', '1', 'StudyComponentStatusID'], + '0x1060': ['LO', '1', 'RequestedProcedureDescription'], + '0x1064': ['SQ', '1', 'RequestedProcedureCodeSequence'], + '0x1070': ['LO', '1', 'RequestedContrastAgent'], + '0x4000': ['LT', '1', 'StudyComments'] + }, + '0x0038': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0004': ['SQ', '1', 'ReferencedPatientAliasSequence'], + '0x0008': ['CS', '1', 'VisitStatusID'], + '0x0010': ['LO', '1', 'AdmissionID'], + '0x0011': ['LO', '1', 'IssuerOfAdmissionID'], + '0x0014': ['SQ', '1', 'IssuerOfAdmissionIDSequence'], + '0x0016': ['LO', '1', 'RouteOfAdmissions'], + '0x001A': ['DA', '1', 'ScheduledAdmissionDate'], + '0x001B': ['TM', '1', 'ScheduledAdmissionTime'], + '0x001C': ['DA', '1', 'ScheduledDischargeDate'], + '0x001D': ['TM', '1', 'ScheduledDischargeTime'], + '0x001E': ['LO', '1', 'ScheduledPatientInstitutionResidence'], + '0x0020': ['DA', '1', 'AdmittingDate'], + '0x0021': ['TM', '1', 'AdmittingTime'], + '0x0030': ['DA', '1', 'DischargeDate'], + '0x0032': ['TM', '1', 'DischargeTime'], + '0x0040': ['LO', '1', 'DischargeDiagnosisDescription'], + '0x0044': ['SQ', '1', 'DischargeDiagnosisCodeSequence'], + '0x0050': ['LO', '1', 'SpecialNeeds'], + '0x0060': ['LO', '1', 'ServiceEpisodeID'], + '0x0061': ['LO', '1', 'IssuerOfServiceEpisodeID'], + '0x0062': ['LO', '1', 'ServiceEpisodeDescription'], + '0x0064': ['SQ', '1', 'IssuerOfServiceEpisodeIDSequence'], + '0x0100': ['SQ', '1', 'PertinentDocumentsSequence'], + '0x0101': ['SQ', '1', 'PertinentResourcesSequence'], + '0x0102': ['LO', '1', 'ResourceDescription'], + '0x0300': ['LO', '1', 'CurrentPatientLocation'], + '0x0400': ['LO', '1', 'PatientInstitutionResidence'], + '0x0500': ['LO', '1', 'PatientState'], + '0x0502': ['SQ', '1', 'PatientClinicalTrialParticipationSequence'], + '0x4000': ['LT', '1', 'VisitComments'] + }, + '0x003A': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0004': ['CS', '1', 'WaveformOriginality'], + '0x0005': ['US', '1', 'NumberOfWaveformChannels'], + '0x0010': ['UL', '1', 'NumberOfWaveformSamples'], + '0x001A': ['DS', '1', 'SamplingFrequency'], + '0x0020': ['SH', '1', 'MultiplexGroupLabel'], + '0x0200': ['SQ', '1', 'ChannelDefinitionSequence'], + '0x0202': ['IS', '1', 'WaveformChannelNumber'], + '0x0203': ['SH', '1', 'ChannelLabel'], + '0x0205': ['CS', '1-n', 'ChannelStatus'], + '0x0208': ['SQ', '1', 'ChannelSourceSequence'], + '0x0209': ['SQ', '1', 'ChannelSourceModifiersSequence'], + '0x020A': ['SQ', '1', 'SourceWaveformSequence'], + '0x020C': ['LO', '1', 'ChannelDerivationDescription'], + '0x0210': ['DS', '1', 'ChannelSensitivity'], + '0x0211': ['SQ', '1', 'ChannelSensitivityUnitsSequence'], + '0x0212': ['DS', '1', 'ChannelSensitivityCorrectionFactor'], + '0x0213': ['DS', '1', 'ChannelBaseline'], + '0x0214': ['DS', '1', 'ChannelTimeSkew'], + '0x0215': ['DS', '1', 'ChannelSampleSkew'], + '0x0218': ['DS', '1', 'ChannelOffset'], + '0x021A': ['US', '1', 'WaveformBitsStored'], + '0x0220': ['DS', '1', 'FilterLowFrequency'], + '0x0221': ['DS', '1', 'FilterHighFrequency'], + '0x0222': ['DS', '1', 'NotchFilterFrequency'], + '0x0223': ['DS', '1', 'NotchFilterBandwidth'], + '0x0230': ['FL', '1', 'WaveformDataDisplayScale'], + '0x0231': ['US', '3', 'WaveformDisplayBackgroundCIELabValue'], + '0x0240': ['SQ', '1', 'WaveformPresentationGroupSequence'], + '0x0241': ['US', '1', 'PresentationGroupNumber'], + '0x0242': ['SQ', '1', 'ChannelDisplaySequence'], + '0x0244': ['US', '3', 'ChannelRecommendedDisplayCIELabValue'], + '0x0245': ['FL', '1', 'ChannelPosition'], + '0x0246': ['CS', '1', 'DisplayShadingFlag'], + '0x0247': ['FL', '1', 'FractionalChannelDisplayScale'], + '0x0248': ['FL', '1', 'AbsoluteChannelDisplayScale'], + '0x0300': ['SQ', '1', 'MultiplexedAudioChannelsDescriptionCodeSequence'], + '0x0301': ['IS', '1', 'ChannelIdentificationCode'], + '0x0302': ['CS', '1', 'ChannelMode'] + }, + '0x0040': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['AE', '1-n', 'ScheduledStationAETitle'], + '0x0002': ['DA', '1', 'ScheduledProcedureStepStartDate'], + '0x0003': ['TM', '1', 'ScheduledProcedureStepStartTime'], + '0x0004': ['DA', '1', 'ScheduledProcedureStepEndDate'], + '0x0005': ['TM', '1', 'ScheduledProcedureStepEndTime'], + '0x0006': ['PN', '1', 'ScheduledPerformingPhysicianName'], + '0x0007': ['LO', '1', 'ScheduledProcedureStepDescription'], + '0x0008': ['SQ', '1', 'ScheduledProtocolCodeSequence'], + '0x0009': ['SH', '1', 'ScheduledProcedureStepID'], + '0x000A': ['SQ', '1', 'StageCodeSequence'], + '0x000B': ['SQ', '1', 'ScheduledPerformingPhysicianIdentificationSequence'], + '0x0010': ['SH', '1-n', 'ScheduledStationName'], + '0x0011': ['SH', '1', 'ScheduledProcedureStepLocation'], + '0x0012': ['LO', '1', 'PreMedication'], + '0x0020': ['CS', '1', 'ScheduledProcedureStepStatus'], + '0x0026': ['SQ', '1', 'OrderPlacerIdentifierSequence'], + '0x0027': ['SQ', '1', 'OrderFillerIdentifierSequence'], + '0x0031': ['UT', '1', 'LocalNamespaceEntityID'], + '0x0032': ['UT', '1', 'UniversalEntityID'], + '0x0033': ['CS', '1', 'UniversalEntityIDType'], + '0x0035': ['CS', '1', 'IdentifierTypeCode'], + '0x0036': ['SQ', '1', 'AssigningFacilitySequence'], + '0x0039': ['SQ', '1', 'AssigningJurisdictionCodeSequence'], + '0x003A': ['SQ', '1', 'AssigningAgencyOrDepartmentCodeSequence'], + '0x0100': ['SQ', '1', 'ScheduledProcedureStepSequence'], + '0x0220': ['SQ', '1', 'ReferencedNonImageCompositeSOPInstanceSequence'], + '0x0241': ['AE', '1', 'PerformedStationAETitle'], + '0x0242': ['SH', '1', 'PerformedStationName'], + '0x0243': ['SH', '1', 'PerformedLocation'], + '0x0244': ['DA', '1', 'PerformedProcedureStepStartDate'], + '0x0245': ['TM', '1', 'PerformedProcedureStepStartTime'], + '0x0250': ['DA', '1', 'PerformedProcedureStepEndDate'], + '0x0251': ['TM', '1', 'PerformedProcedureStepEndTime'], + '0x0252': ['CS', '1', 'PerformedProcedureStepStatus'], + '0x0253': ['SH', '1', 'PerformedProcedureStepID'], + '0x0254': ['LO', '1', 'PerformedProcedureStepDescription'], + '0x0255': ['LO', '1', 'PerformedProcedureTypeDescription'], + '0x0260': ['SQ', '1', 'PerformedProtocolCodeSequence'], + '0x0261': ['CS', '1', 'PerformedProtocolType'], + '0x0270': ['SQ', '1', 'ScheduledStepAttributesSequence'], + '0x0275': ['SQ', '1', 'RequestAttributesSequence'], + '0x0280': ['ST', '1', 'CommentsOnThePerformedProcedureStep'], + '0x0281': ['SQ', '1', 'PerformedProcedureStepDiscontinuationReasonCodeSequence'], + '0x0293': ['SQ', '1', 'QuantitySequence'], + '0x0294': ['DS', '1', 'Quantity'], + '0x0295': ['SQ', '1', 'MeasuringUnitsSequence'], + '0x0296': ['SQ', '1', 'BillingItemSequence'], + '0x0300': ['US', '1', 'TotalTimeOfFluoroscopy'], + '0x0301': ['US', '1', 'TotalNumberOfExposures'], + '0x0302': ['US', '1', 'EntranceDose'], + '0x0303': ['US', '1-2', 'ExposedArea'], + '0x0306': ['DS', '1', 'DistanceSourceToEntrance'], + '0x0307': ['DS', '1', 'DistanceSourceToSupport'], + '0x030E': ['SQ', '1', 'ExposureDoseSequence'], + '0x0310': ['ST', '1', 'CommentsOnRadiationDose'], + '0x0312': ['DS', '1', 'XRayOutput'], + '0x0314': ['DS', '1', 'HalfValueLayer'], + '0x0316': ['DS', '1', 'OrganDose'], + '0x0318': ['CS', '1', 'OrganExposed'], + '0x0320': ['SQ', '1', 'BillingProcedureStepSequence'], + '0x0321': ['SQ', '1', 'FilmConsumptionSequence'], + '0x0324': ['SQ', '1', 'BillingSuppliesAndDevicesSequence'], + '0x0330': ['SQ', '1', 'ReferencedProcedureStepSequence'], + '0x0340': ['SQ', '1', 'PerformedSeriesSequence'], + '0x0400': ['LT', '1', 'CommentsOnTheScheduledProcedureStep'], + '0x0440': ['SQ', '1', 'ProtocolContextSequence'], + '0x0441': ['SQ', '1', 'ContentItemModifierSequence'], + '0x0500': ['SQ', '1', 'ScheduledSpecimenSequence'], + '0x050A': ['LO', '1', 'SpecimenAccessionNumber'], + '0x0512': ['LO', '1', 'ContainerIdentifier'], + '0x0513': ['SQ', '1', 'IssuerOfTheContainerIdentifierSequence'], + '0x0515': ['SQ', '1', 'AlternateContainerIdentifierSequence'], + '0x0518': ['SQ', '1', 'ContainerTypeCodeSequence'], + '0x051A': ['LO', '1', 'ContainerDescription'], + '0x0520': ['SQ', '1', 'ContainerComponentSequence'], + '0x0550': ['SQ', '1', 'SpecimenSequence'], + '0x0551': ['LO', '1', 'SpecimenIdentifier'], + '0x0552': ['SQ', '1', 'SpecimenDescriptionSequenceTrial'], + '0x0553': ['ST', '1', 'SpecimenDescriptionTrial'], + '0x0554': ['UI', '1', 'SpecimenUID'], + '0x0555': ['SQ', '1', 'AcquisitionContextSequence'], + '0x0556': ['ST', '1', 'AcquisitionContextDescription'], + '0x059A': ['SQ', '1', 'SpecimenTypeCodeSequence'], + '0x0560': ['SQ', '1', 'SpecimenDescriptionSequence'], + '0x0562': ['SQ', '1', 'IssuerOfTheSpecimenIdentifierSequence'], + '0x0600': ['LO', '1', 'SpecimenShortDescription'], + '0x0602': ['UT', '1', 'SpecimenDetailedDescription'], + '0x0610': ['SQ', '1', 'SpecimenPreparationSequence'], + '0x0612': ['SQ', '1', 'SpecimenPreparationStepContentItemSequence'], + '0x0620': ['SQ', '1', 'SpecimenLocalizationContentItemSequence'], + '0x06FA': ['LO', '1', 'SlideIdentifier'], + '0x071A': ['SQ', '1', 'ImageCenterPointCoordinatesSequence'], + '0x072A': ['DS', '1', 'XOffsetInSlideCoordinateSystem'], + '0x073A': ['DS', '1', 'YOffsetInSlideCoordinateSystem'], + '0x074A': ['DS', '1', 'ZOffsetInSlideCoordinateSystem'], + '0x08D8': ['SQ', '1', 'PixelSpacingSequence'], + '0x08DA': ['SQ', '1', 'CoordinateSystemAxisCodeSequence'], + '0x08EA': ['SQ', '1', 'MeasurementUnitsCodeSequence'], + '0x09F8': ['SQ', '1', 'VitalStainCodeSequenceTrial'], + '0x1001': ['SH', '1', 'RequestedProcedureID'], + '0x1002': ['LO', '1', 'ReasonForTheRequestedProcedure'], + '0x1003': ['SH', '1', 'RequestedProcedurePriority'], + '0x1004': ['LO', '1', 'PatientTransportArrangements'], + '0x1005': ['LO', '1', 'RequestedProcedureLocation'], + '0x1006': ['SH', '1', 'PlacerOrderNumberProcedure'], + '0x1007': ['SH', '1', 'FillerOrderNumberProcedure'], + '0x1008': ['LO', '1', 'ConfidentialityCode'], + '0x1009': ['SH', '1', 'ReportingPriority'], + '0x100A': ['SQ', '1', 'ReasonForRequestedProcedureCodeSequence'], + '0x1010': ['PN', '1-n', 'NamesOfIntendedRecipientsOfResults'], + '0x1011': ['SQ', '1', 'IntendedRecipientsOfResultsIdentificationSequence'], + '0x1012': ['SQ', '1', 'ReasonForPerformedProcedureCodeSequence'], + '0x1060': ['LO', '1', 'RequestedProcedureDescriptionTrial'], + '0x1101': ['SQ', '1', 'PersonIdentificationCodeSequence'], + '0x1102': ['ST', '1', 'PersonAddress'], + '0x1103': ['LO', '1-n', 'PersonTelephoneNumbers'], + '0x1104': ['LT', '1', 'PersonTelecomInformation'], + '0x1400': ['LT', '1', 'RequestedProcedureComments'], + '0x2001': ['LO', '1', 'ReasonForTheImagingServiceRequest'], + '0x2004': ['DA', '1', 'IssueDateOfImagingServiceRequest'], + '0x2005': ['TM', '1', 'IssueTimeOfImagingServiceRequest'], + '0x2006': ['SH', '1', 'PlacerOrderNumberImagingServiceRequestRetired'], + '0x2007': ['SH', '1', 'FillerOrderNumberImagingServiceRequestRetired'], + '0x2008': ['PN', '1', 'OrderEnteredBy'], + '0x2009': ['SH', '1', 'OrderEntererLocation'], + '0x2010': ['SH', '1', 'OrderCallbackPhoneNumber'], + '0x2011': ['LT', '1', 'OrderCallbackTelecomInformation'], + '0x2016': ['LO', '1', 'PlacerOrderNumberImagingServiceRequest'], + '0x2017': ['LO', '1', 'FillerOrderNumberImagingServiceRequest'], + '0x2400': ['LT', '1', 'ImagingServiceRequestComments'], + '0x3001': ['LO', '1', 'ConfidentialityConstraintOnPatientDataDescription'], + '0x4001': ['CS', '1', 'GeneralPurposeScheduledProcedureStepStatus'], + '0x4002': ['CS', '1', 'GeneralPurposePerformedProcedureStepStatus'], + '0x4003': ['CS', '1', 'GeneralPurposeScheduledProcedureStepPriority'], + '0x4004': ['SQ', '1', 'ScheduledProcessingApplicationsCodeSequence'], + '0x4005': ['DT', '1', 'ScheduledProcedureStepStartDateTime'], + '0x4006': ['CS', '1', 'MultipleCopiesFlag'], + '0x4007': ['SQ', '1', 'PerformedProcessingApplicationsCodeSequence'], + '0x4009': ['SQ', '1', 'HumanPerformerCodeSequence'], + '0x4010': ['DT', '1', 'ScheduledProcedureStepModificationDateTime'], + '0x4011': ['DT', '1', 'ExpectedCompletionDateTime'], + '0x4015': ['SQ', '1', 'ResultingGeneralPurposePerformedProcedureStepsSequence'], + '0x4016': ['SQ', '1', 'ReferencedGeneralPurposeScheduledProcedureStepSequence'], + '0x4018': ['SQ', '1', 'ScheduledWorkitemCodeSequence'], + '0x4019': ['SQ', '1', 'PerformedWorkitemCodeSequence'], + '0x4020': ['CS', '1', 'InputAvailabilityFlag'], + '0x4021': ['SQ', '1', 'InputInformationSequence'], + '0x4022': ['SQ', '1', 'RelevantInformationSequence'], + '0x4023': ['UI', '1', 'ReferencedGeneralPurposeScheduledProcedureStepTransactionUID'], + '0x4025': ['SQ', '1', 'ScheduledStationNameCodeSequence'], + '0x4026': ['SQ', '1', 'ScheduledStationClassCodeSequence'], + '0x4027': ['SQ', '1', 'ScheduledStationGeographicLocationCodeSequence'], + '0x4028': ['SQ', '1', 'PerformedStationNameCodeSequence'], + '0x4029': ['SQ', '1', 'PerformedStationClassCodeSequence'], + '0x4030': ['SQ', '1', 'PerformedStationGeographicLocationCodeSequence'], + '0x4031': ['SQ', '1', 'RequestedSubsequentWorkitemCodeSequence'], + '0x4032': ['SQ', '1', 'NonDICOMOutputCodeSequence'], + '0x4033': ['SQ', '1', 'OutputInformationSequence'], + '0x4034': ['SQ', '1', 'ScheduledHumanPerformersSequence'], + '0x4035': ['SQ', '1', 'ActualHumanPerformersSequence'], + '0x4036': ['LO', '1', 'HumanPerformerOrganization'], + '0x4037': ['PN', '1', 'HumanPerformerName'], + '0x4040': ['CS', '1', 'RawDataHandling'], + '0x4041': ['CS', '1', 'InputReadinessState'], + '0x4050': ['DT', '1', 'PerformedProcedureStepStartDateTime'], + '0x4051': ['DT', '1', 'PerformedProcedureStepEndDateTime'], + '0x4052': ['DT', '1', 'ProcedureStepCancellationDateTime'], + '0x8302': ['DS', '1', 'EntranceDoseInmGy'], + '0x9092': ['SQ', '1', 'ParametricMapFrameTypeSequence'], + '0x9094': ['SQ', '1', 'ReferencedImageRealWorldValueMappingSequence'], + '0x9096': ['SQ', '1', 'RealWorldValueMappingSequence'], + '0x9098': ['SQ', '1', 'PixelValueMappingCodeSequence'], + '0x9210': ['SH', '1', 'LUTLabel'], + '0x9211': ['xs', '1', 'RealWorldValueLastValueMapped'], + '0x9212': ['FD', '1-n', 'RealWorldValueLUTData'], + '0x9216': ['xs', '1', 'RealWorldValueFirstValueMapped'], + '0x9220': ['SQ', '1', 'QuantityDefinitionSequence'], + '0x9224': ['FD', '1', 'RealWorldValueIntercept'], + '0x9225': ['FD', '1', 'RealWorldValueSlope'], + '0xA007': ['CS', '1', 'FindingsFlagTrial'], + '0xA010': ['CS', '1', 'RelationshipType'], + '0xA020': ['SQ', '1', 'FindingsSequenceTrial'], + '0xA021': ['UI', '1', 'FindingsGroupUIDTrial'], + '0xA022': ['UI', '1', 'ReferencedFindingsGroupUIDTrial'], + '0xA023': ['DA', '1', 'FindingsGroupRecordingDateTrial'], + '0xA024': ['TM', '1', 'FindingsGroupRecordingTimeTrial'], + '0xA026': ['SQ', '1', 'FindingsSourceCategoryCodeSequenceTrial'], + '0xA027': ['LO', '1', 'VerifyingOrganization'], + '0xA028': ['SQ', '1', 'DocumentingOrganizationIdentifierCodeSequenceTrial'], + '0xA030': ['DT', '1', 'VerificationDateTime'], + '0xA032': ['DT', '1', 'ObservationDateTime'], + '0xA040': ['CS', '1', 'ValueType'], + '0xA043': ['SQ', '1', 'ConceptNameCodeSequence'], + '0xA047': ['LO', '1', 'MeasurementPrecisionDescriptionTrial'], + '0xA050': ['CS', '1', 'ContinuityOfContent'], + '0xA057': ['CS', '1-n', 'UrgencyOrPriorityAlertsTrial'], + '0xA060': ['LO', '1', 'SequencingIndicatorTrial'], + '0xA066': ['SQ', '1', 'DocumentIdentifierCodeSequenceTrial'], + '0xA067': ['PN', '1', 'DocumentAuthorTrial'], + '0xA068': ['SQ', '1', 'DocumentAuthorIdentifierCodeSequenceTrial'], + '0xA070': ['SQ', '1', 'IdentifierCodeSequenceTrial'], + '0xA073': ['SQ', '1', 'VerifyingObserverSequence'], + '0xA074': ['OB', '1', 'ObjectBinaryIdentifierTrial'], + '0xA075': ['PN', '1', 'VerifyingObserverName'], + '0xA076': ['SQ', '1', 'DocumentingObserverIdentifierCodeSequenceTrial'], + '0xA078': ['SQ', '1', 'AuthorObserverSequence'], + '0xA07A': ['SQ', '1', 'ParticipantSequence'], + '0xA07C': ['SQ', '1', 'CustodialOrganizationSequence'], + '0xA080': ['CS', '1', 'ParticipationType'], + '0xA082': ['DT', '1', 'ParticipationDateTime'], + '0xA084': ['CS', '1', 'ObserverType'], + '0xA085': ['SQ', '1', 'ProcedureIdentifierCodeSequenceTrial'], + '0xA088': ['SQ', '1', 'VerifyingObserverIdentificationCodeSequence'], + '0xA089': ['OB', '1', 'ObjectDirectoryBinaryIdentifierTrial'], + '0xA090': ['SQ', '1', 'EquivalentCDADocumentSequence'], + '0xA0B0': ['US', '2-2n', 'ReferencedWaveformChannels'], + '0xA110': ['DA', '1', 'DateOfDocumentOrVerbalTransactionTrial'], + '0xA112': ['TM', '1', 'TimeOfDocumentCreationOrVerbalTransactionTrial'], + '0xA120': ['DT', '1', 'DateTime'], + '0xA121': ['DA', '1', 'Date'], + '0xA122': ['TM', '1', 'Time'], + '0xA123': ['PN', '1', 'PersonName'], + '0xA124': ['UI', '1', 'UID'], + '0xA125': ['CS', '2', 'ReportStatusIDTrial'], + '0xA130': ['CS', '1', 'TemporalRangeType'], + '0xA132': ['UL', '1-n', 'ReferencedSamplePositions'], + '0xA136': ['US', '1-n', 'ReferencedFrameNumbers'], + '0xA138': ['DS', '1-n', 'ReferencedTimeOffsets'], + '0xA13A': ['DT', '1-n', 'ReferencedDateTime'], + '0xA160': ['UT', '1', 'TextValue'], + '0xA161': ['FD', '1-n', 'FloatingPointValue'], + '0xA162': ['SL', '1-n', 'RationalNumeratorValue'], + '0xA163': ['UL', '1-n', 'RationalDenominatorValue'], + '0xA167': ['SQ', '1', 'ObservationCategoryCodeSequenceTrial'], + '0xA168': ['SQ', '1', 'ConceptCodeSequence'], + '0xA16A': ['ST', '1', 'BibliographicCitationTrial'], + '0xA170': ['SQ', '1', 'PurposeOfReferenceCodeSequence'], + '0xA171': ['UI', '1', 'ObservationUID'], + '0xA172': ['UI', '1', 'ReferencedObservationUIDTrial'], + '0xA173': ['CS', '1', 'ReferencedObservationClassTrial'], + '0xA174': ['CS', '1', 'ReferencedObjectObservationClassTrial'], + '0xA180': ['US', '1', 'AnnotationGroupNumber'], + '0xA192': ['DA', '1', 'ObservationDateTrial'], + '0xA193': ['TM', '1', 'ObservationTimeTrial'], + '0xA194': ['CS', '1', 'MeasurementAutomationTrial'], + '0xA195': ['SQ', '1', 'ModifierCodeSequence'], + '0xA224': ['ST', '1', 'IdentificationDescriptionTrial'], + '0xA290': ['CS', '1', 'CoordinatesSetGeometricTypeTrial'], + '0xA296': ['SQ', '1', 'AlgorithmCodeSequenceTrial'], + '0xA297': ['ST', '1', 'AlgorithmDescriptionTrial'], + '0xA29A': ['SL', '2-2n', 'PixelCoordinatesSetTrial'], + '0xA300': ['SQ', '1', 'MeasuredValueSequence'], + '0xA301': ['SQ', '1', 'NumericValueQualifierCodeSequence'], + '0xA307': ['PN', '1', 'CurrentObserverTrial'], + '0xA30A': ['DS', '1-n', 'NumericValue'], + '0xA313': ['SQ', '1', 'ReferencedAccessionSequenceTrial'], + '0xA33A': ['ST', '1', 'ReportStatusCommentTrial'], + '0xA340': ['SQ', '1', 'ProcedureContextSequenceTrial'], + '0xA352': ['PN', '1', 'VerbalSourceTrial'], + '0xA353': ['ST', '1', 'AddressTrial'], + '0xA354': ['LO', '1', 'TelephoneNumberTrial'], + '0xA358': ['SQ', '1', 'VerbalSourceIdentifierCodeSequenceTrial'], + '0xA360': ['SQ', '1', 'PredecessorDocumentsSequence'], + '0xA370': ['SQ', '1', 'ReferencedRequestSequence'], + '0xA372': ['SQ', '1', 'PerformedProcedureCodeSequence'], + '0xA375': ['SQ', '1', 'CurrentRequestedProcedureEvidenceSequence'], + '0xA380': ['SQ', '1', 'ReportDetailSequenceTrial'], + '0xA385': ['SQ', '1', 'PertinentOtherEvidenceSequence'], + '0xA390': ['SQ', '1', 'HL7StructuredDocumentReferenceSequence'], + '0xA402': ['UI', '1', 'ObservationSubjectUIDTrial'], + '0xA403': ['CS', '1', 'ObservationSubjectClassTrial'], + '0xA404': ['SQ', '1', 'ObservationSubjectTypeCodeSequenceTrial'], + '0xA491': ['CS', '1', 'CompletionFlag'], + '0xA492': ['LO', '1', 'CompletionFlagDescription'], + '0xA493': ['CS', '1', 'VerificationFlag'], + '0xA494': ['CS', '1', 'ArchiveRequested'], + '0xA496': ['CS', '1', 'PreliminaryFlag'], + '0xA504': ['SQ', '1', 'ContentTemplateSequence'], + '0xA525': ['SQ', '1', 'IdenticalDocumentsSequence'], + '0xA600': ['CS', '1', 'ObservationSubjectContextFlagTrial'], + '0xA601': ['CS', '1', 'ObserverContextFlagTrial'], + '0xA603': ['CS', '1', 'ProcedureContextFlagTrial'], + '0xA730': ['SQ', '1', 'ContentSequence'], + '0xA731': ['SQ', '1', 'RelationshipSequenceTrial'], + '0xA732': ['SQ', '1', 'RelationshipTypeCodeSequenceTrial'], + '0xA744': ['SQ', '1', 'LanguageCodeSequenceTrial'], + '0xA992': ['ST', '1', 'UniformResourceLocatorTrial'], + '0xB020': ['SQ', '1', 'WaveformAnnotationSequence'], + '0xDB00': ['CS', '1', 'TemplateIdentifier'], + '0xDB06': ['DT', '1', 'TemplateVersion'], + '0xDB07': ['DT', '1', 'TemplateLocalVersion'], + '0xDB0B': ['CS', '1', 'TemplateExtensionFlag'], + '0xDB0C': ['UI', '1', 'TemplateExtensionOrganizationUID'], + '0xDB0D': ['UI', '1', 'TemplateExtensionCreatorUID'], + '0xDB73': ['UL', '1-n', 'ReferencedContentItemIdentifier'], + '0xE001': ['ST', '1', 'HL7InstanceIdentifier'], + '0xE004': ['DT', '1', 'HL7DocumentEffectiveTime'], + '0xE006': ['SQ', '1', 'HL7DocumentTypeCodeSequence'], + '0xE008': ['SQ', '1', 'DocumentClassCodeSequence'], + '0xE010': ['UR', '1', 'RetrieveURI'], + '0xE011': ['UI', '1', 'RetrieveLocationUID'], + '0xE020': ['CS', '1', 'TypeOfInstances'], + '0xE021': ['SQ', '1', 'DICOMRetrievalSequence'], + '0xE022': ['SQ', '1', 'DICOMMediaRetrievalSequence'], + '0xE023': ['SQ', '1', 'WADORetrievalSequence'], + '0xE024': ['SQ', '1', 'XDSRetrievalSequence'], + '0xE025': ['SQ', '1', 'WADORSRetrievalSequence'], + '0xE030': ['UI', '1', 'RepositoryUniqueID'], + '0xE031': ['UI', '1', 'HomeCommunityID'] + }, + '0x0042': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['ST', '1', 'DocumentTitle'], + '0x0011': ['OB', '1', 'EncapsulatedDocument'], + '0x0012': ['LO', '1', 'MIMETypeOfEncapsulatedDocument'], + '0x0013': ['SQ', '1', 'SourceInstanceSequence'], + '0x0014': ['LO', '1-n', 'ListOfMIMETypes'] + }, + '0x0044': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['ST', '1', 'ProductPackageIdentifier'], + '0x0002': ['CS', '1', 'SubstanceAdministrationApproval'], + '0x0003': ['LT', '1', 'ApprovalStatusFurtherDescription'], + '0x0004': ['DT', '1', 'ApprovalStatusDateTime'], + '0x0007': ['SQ', '1', 'ProductTypeCodeSequence'], + '0x0008': ['LO', '1-n', 'ProductName'], + '0x0009': ['LT', '1', 'ProductDescription'], + '0x000A': ['LO', '1', 'ProductLotIdentifier'], + '0x000B': ['DT', '1', 'ProductExpirationDateTime'], + '0x0010': ['DT', '1', 'SubstanceAdministrationDateTime'], + '0x0011': ['LO', '1', 'SubstanceAdministrationNotes'], + '0x0012': ['LO', '1', 'SubstanceAdministrationDeviceID'], + '0x0013': ['SQ', '1', 'ProductParameterSequence'], + '0x0019': ['SQ', '1', 'SubstanceAdministrationParameterSequence'] + }, + '0x0046': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0012': ['LO', '1', 'LensDescription'], + '0x0014': ['SQ', '1', 'RightLensSequence'], + '0x0015': ['SQ', '1', 'LeftLensSequence'], + '0x0016': ['SQ', '1', 'UnspecifiedLateralityLensSequence'], + '0x0018': ['SQ', '1', 'CylinderSequence'], + '0x0028': ['SQ', '1', 'PrismSequence'], + '0x0030': ['FD', '1', 'HorizontalPrismPower'], + '0x0032': ['CS', '1', 'HorizontalPrismBase'], + '0x0034': ['FD', '1', 'VerticalPrismPower'], + '0x0036': ['CS', '1', 'VerticalPrismBase'], + '0x0038': ['CS', '1', 'LensSegmentType'], + '0x0040': ['FD', '1', 'OpticalTransmittance'], + '0x0042': ['FD', '1', 'ChannelWidth'], + '0x0044': ['FD', '1', 'PupilSize'], + '0x0046': ['FD', '1', 'CornealSize'], + '0x0050': ['SQ', '1', 'AutorefractionRightEyeSequence'], + '0x0052': ['SQ', '1', 'AutorefractionLeftEyeSequence'], + '0x0060': ['FD', '1', 'DistancePupillaryDistance'], + '0x0062': ['FD', '1', 'NearPupillaryDistance'], + '0x0063': ['FD', '1', 'IntermediatePupillaryDistance'], + '0x0064': ['FD', '1', 'OtherPupillaryDistance'], + '0x0070': ['SQ', '1', 'KeratometryRightEyeSequence'], + '0x0071': ['SQ', '1', 'KeratometryLeftEyeSequence'], + '0x0074': ['SQ', '1', 'SteepKeratometricAxisSequence'], + '0x0075': ['FD', '1', 'RadiusOfCurvature'], + '0x0076': ['FD', '1', 'KeratometricPower'], + '0x0077': ['FD', '1', 'KeratometricAxis'], + '0x0080': ['SQ', '1', 'FlatKeratometricAxisSequence'], + '0x0092': ['CS', '1', 'BackgroundColor'], + '0x0094': ['CS', '1', 'Optotype'], + '0x0095': ['CS', '1', 'OptotypePresentation'], + '0x0097': ['SQ', '1', 'SubjectiveRefractionRightEyeSequence'], + '0x0098': ['SQ', '1', 'SubjectiveRefractionLeftEyeSequence'], + '0x0100': ['SQ', '1', 'AddNearSequence'], + '0x0101': ['SQ', '1', 'AddIntermediateSequence'], + '0x0102': ['SQ', '1', 'AddOtherSequence'], + '0x0104': ['FD', '1', 'AddPower'], + '0x0106': ['FD', '1', 'ViewingDistance'], + '0x0121': ['SQ', '1', 'VisualAcuityTypeCodeSequence'], + '0x0122': ['SQ', '1', 'VisualAcuityRightEyeSequence'], + '0x0123': ['SQ', '1', 'VisualAcuityLeftEyeSequence'], + '0x0124': ['SQ', '1', 'VisualAcuityBothEyesOpenSequence'], + '0x0125': ['CS', '1', 'ViewingDistanceType'], + '0x0135': ['SS', '2', 'VisualAcuityModifiers'], + '0x0137': ['FD', '1', 'DecimalVisualAcuity'], + '0x0139': ['LO', '1', 'OptotypeDetailedDefinition'], + '0x0145': ['SQ', '1', 'ReferencedRefractiveMeasurementsSequence'], + '0x0146': ['FD', '1', 'SpherePower'], + '0x0147': ['FD', '1', 'CylinderPower'], + '0x0201': ['CS', '1', 'CornealTopographySurface'], + '0x0202': ['FL', '2', 'CornealVertexLocation'], + '0x0203': ['FL', '1', 'PupilCentroidXCoordinate'], + '0x0204': ['FL', '1', 'PupilCentroidYCoordinate'], + '0x0205': ['FL', '1', 'EquivalentPupilRadius'], + '0x0207': ['SQ', '1', 'CornealTopographyMapTypeCodeSequence'], + '0x0208': ['IS', '2-2n', 'VerticesOfTheOutlineOfPupil'], + '0x0210': ['SQ', '1', 'CornealTopographyMappingNormalsSequence'], + '0x0211': ['SQ', '1', 'MaximumCornealCurvatureSequence'], + '0x0212': ['FL', '1', 'MaximumCornealCurvature'], + '0x0213': ['FL', '2', 'MaximumCornealCurvatureLocation'], + '0x0215': ['SQ', '1', 'MinimumKeratometricSequence'], + '0x0218': ['SQ', '1', 'SimulatedKeratometricCylinderSequence'], + '0x0220': ['FL', '1', 'AverageCornealPower'], + '0x0224': ['FL', '1', 'CornealISValue'], + '0x0227': ['FL', '1', 'AnalyzedArea'], + '0x0230': ['FL', '1', 'SurfaceRegularityIndex'], + '0x0232': ['FL', '1', 'SurfaceAsymmetryIndex'], + '0x0234': ['FL', '1', 'CornealEccentricityIndex'], + '0x0236': ['FL', '1', 'KeratoconusPredictionIndex'], + '0x0238': ['FL', '1', 'DecimalPotentialVisualAcuity'], + '0x0242': ['CS', '1', 'CornealTopographyMapQualityEvaluation'], + '0x0244': ['SQ', '1', 'SourceImageCornealProcessedDataSequence'], + '0x0247': ['FL', '3', 'CornealPointLocation'], + '0x0248': ['CS', '1', 'CornealPointEstimated'], + '0x0249': ['FL', '1', 'AxialPower'], + '0x0250': ['FL', '1', 'TangentialPower'], + '0x0251': ['FL', '1', 'RefractivePower'], + '0x0252': ['FL', '1', 'RelativeElevation'], + '0x0253': ['FL', '1', 'CornealWavefront'] + }, + '0x0048': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['FL', '1', 'ImagedVolumeWidth'], + '0x0002': ['FL', '1', 'ImagedVolumeHeight'], + '0x0003': ['FL', '1', 'ImagedVolumeDepth'], + '0x0006': ['UL', '1', 'TotalPixelMatrixColumns'], + '0x0007': ['UL', '1', 'TotalPixelMatrixRows'], + '0x0008': ['SQ', '1', 'TotalPixelMatrixOriginSequence'], + '0x0010': ['CS', '1', 'SpecimenLabelInImage'], + '0x0011': ['CS', '1', 'FocusMethod'], + '0x0012': ['CS', '1', 'ExtendedDepthOfField'], + '0x0013': ['US', '1', 'NumberOfFocalPlanes'], + '0x0014': ['FL', '1', 'DistanceBetweenFocalPlanes'], + '0x0015': ['US', '3', 'RecommendedAbsentPixelCIELabValue'], + '0x0100': ['SQ', '1', 'IlluminatorTypeCodeSequence'], + '0x0102': ['DS', '6', 'ImageOrientationSlide'], + '0x0105': ['SQ', '1', 'OpticalPathSequence'], + '0x0106': ['SH', '1', 'OpticalPathIdentifier'], + '0x0107': ['ST', '1', 'OpticalPathDescription'], + '0x0108': ['SQ', '1', 'IlluminationColorCodeSequence'], + '0x0110': ['SQ', '1', 'SpecimenReferenceSequence'], + '0x0111': ['DS', '1', 'CondenserLensPower'], + '0x0112': ['DS', '1', 'ObjectiveLensPower'], + '0x0113': ['DS', '1', 'ObjectiveLensNumericalAperture'], + '0x0120': ['SQ', '1', 'PaletteColorLookupTableSequence'], + '0x0200': ['SQ', '1', 'ReferencedImageNavigationSequence'], + '0x0201': ['US', '2', 'TopLeftHandCornerOfLocalizerArea'], + '0x0202': ['US', '2', 'BottomRightHandCornerOfLocalizerArea'], + '0x0207': ['SQ', '1', 'OpticalPathIdentificationSequence'], + '0x021A': ['SQ', '1', 'PlanePositionSlideSequence'], + '0x021E': ['SL', '1', 'ColumnPositionInTotalImagePixelMatrix'], + '0x021F': ['SL', '1', 'RowPositionInTotalImagePixelMatrix'], + '0x0301': ['CS', '1', 'PixelOriginInterpretation'] + }, + '0x0050': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0004': ['CS', '1', 'CalibrationImage'], + '0x0010': ['SQ', '1', 'DeviceSequence'], + '0x0012': ['SQ', '1', 'ContainerComponentTypeCodeSequence'], + '0x0013': ['FD', '1', 'ContainerComponentThickness'], + '0x0014': ['DS', '1', 'DeviceLength'], + '0x0015': ['FD', '1', 'ContainerComponentWidth'], + '0x0016': ['DS', '1', 'DeviceDiameter'], + '0x0017': ['CS', '1', 'DeviceDiameterUnits'], + '0x0018': ['DS', '1', 'DeviceVolume'], + '0x0019': ['DS', '1', 'InterMarkerDistance'], + '0x001A': ['CS', '1', 'ContainerComponentMaterial'], + '0x001B': ['LO', '1', 'ContainerComponentID'], + '0x001C': ['FD', '1', 'ContainerComponentLength'], + '0x001D': ['FD', '1', 'ContainerComponentDiameter'], + '0x001E': ['LO', '1', 'ContainerComponentDescription'], + '0x0020': ['LO', '1', 'DeviceDescription'] + }, + '0x0052': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['FL', '1', 'ContrastBolusIngredientPercentByVolume'], + '0x0002': ['FD', '1', 'OCTFocalDistance'], + '0x0003': ['FD', '1', 'BeamSpotSize'], + '0x0004': ['FD', '1', 'EffectiveRefractiveIndex'], + '0x0006': ['CS', '1', 'OCTAcquisitionDomain'], + '0x0007': ['FD', '1', 'OCTOpticalCenterWavelength'], + '0x0008': ['FD', '1', 'AxialResolution'], + '0x0009': ['FD', '1', 'RangingDepth'], + '0x0011': ['FD', '1', 'ALineRate'], + '0x0012': ['US', '1', 'ALinesPerFrame'], + '0x0013': ['FD', '1', 'CatheterRotationalRate'], + '0x0014': ['FD', '1', 'ALinePixelSpacing'], + '0x0016': ['SQ', '1', 'ModeOfPercutaneousAccessSequence'], + '0x0025': ['SQ', '1', 'IntravascularOCTFrameTypeSequence'], + '0x0026': ['CS', '1', 'OCTZOffsetApplied'], + '0x0027': ['SQ', '1', 'IntravascularFrameContentSequence'], + '0x0028': ['FD', '1', 'IntravascularLongitudinalDistance'], + '0x0029': ['SQ', '1', 'IntravascularOCTFrameContentSequence'], + '0x0030': ['SS', '1', 'OCTZOffsetCorrection'], + '0x0031': ['CS', '1', 'CatheterDirectionOfRotation'], + '0x0033': ['FD', '1', 'SeamLineLocation'], + '0x0034': ['FD', '1', 'FirstALineLocation'], + '0x0036': ['US', '1', 'SeamLineIndex'], + '0x0038': ['US', '1', 'NumberOfPaddedALines'], + '0x0039': ['CS', '1', 'InterpolationType'], + '0x003A': ['CS', '1', 'RefractiveIndexApplied'] + }, + '0x0054': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['US', '1-n', 'EnergyWindowVector'], + '0x0011': ['US', '1', 'NumberOfEnergyWindows'], + '0x0012': ['SQ', '1', 'EnergyWindowInformationSequence'], + '0x0013': ['SQ', '1', 'EnergyWindowRangeSequence'], + '0x0014': ['DS', '1', 'EnergyWindowLowerLimit'], + '0x0015': ['DS', '1', 'EnergyWindowUpperLimit'], + '0x0016': ['SQ', '1', 'RadiopharmaceuticalInformationSequence'], + '0x0017': ['IS', '1', 'ResidualSyringeCounts'], + '0x0018': ['SH', '1', 'EnergyWindowName'], + '0x0020': ['US', '1-n', 'DetectorVector'], + '0x0021': ['US', '1', 'NumberOfDetectors'], + '0x0022': ['SQ', '1', 'DetectorInformationSequence'], + '0x0030': ['US', '1-n', 'PhaseVector'], + '0x0031': ['US', '1', 'NumberOfPhases'], + '0x0032': ['SQ', '1', 'PhaseInformationSequence'], + '0x0033': ['US', '1', 'NumberOfFramesInPhase'], + '0x0036': ['IS', '1', 'PhaseDelay'], + '0x0038': ['IS', '1', 'PauseBetweenFrames'], + '0x0039': ['CS', '1', 'PhaseDescription'], + '0x0050': ['US', '1-n', 'RotationVector'], + '0x0051': ['US', '1', 'NumberOfRotations'], + '0x0052': ['SQ', '1', 'RotationInformationSequence'], + '0x0053': ['US', '1', 'NumberOfFramesInRotation'], + '0x0060': ['US', '1-n', 'RRIntervalVector'], + '0x0061': ['US', '1', 'NumberOfRRIntervals'], + '0x0062': ['SQ', '1', 'GatedInformationSequence'], + '0x0063': ['SQ', '1', 'DataInformationSequence'], + '0x0070': ['US', '1-n', 'TimeSlotVector'], + '0x0071': ['US', '1', 'NumberOfTimeSlots'], + '0x0072': ['SQ', '1', 'TimeSlotInformationSequence'], + '0x0073': ['DS', '1', 'TimeSlotTime'], + '0x0080': ['US', '1-n', 'SliceVector'], + '0x0081': ['US', '1', 'NumberOfSlices'], + '0x0090': ['US', '1-n', 'AngularViewVector'], + '0x0100': ['US', '1-n', 'TimeSliceVector'], + '0x0101': ['US', '1', 'NumberOfTimeSlices'], + '0x0200': ['DS', '1', 'StartAngle'], + '0x0202': ['CS', '1', 'TypeOfDetectorMotion'], + '0x0210': ['IS', '1-n', 'TriggerVector'], + '0x0211': ['US', '1', 'NumberOfTriggersInPhase'], + '0x0220': ['SQ', '1', 'ViewCodeSequence'], + '0x0222': ['SQ', '1', 'ViewModifierCodeSequence'], + '0x0300': ['SQ', '1', 'RadionuclideCodeSequence'], + '0x0302': ['SQ', '1', 'AdministrationRouteCodeSequence'], + '0x0304': ['SQ', '1', 'RadiopharmaceuticalCodeSequence'], + '0x0306': ['SQ', '1', 'CalibrationDataSequence'], + '0x0308': ['US', '1', 'EnergyWindowNumber'], + '0x0400': ['SH', '1', 'ImageID'], + '0x0410': ['SQ', '1', 'PatientOrientationCodeSequence'], + '0x0412': ['SQ', '1', 'PatientOrientationModifierCodeSequence'], + '0x0414': ['SQ', '1', 'PatientGantryRelationshipCodeSequence'], + '0x0500': ['CS', '1', 'SliceProgressionDirection'], + '0x0501': ['CS', '1', 'ScanProgressionDirection'], + '0x1000': ['CS', '2', 'SeriesType'], + '0x1001': ['CS', '1', 'Units'], + '0x1002': ['CS', '1', 'CountsSource'], + '0x1004': ['CS', '1', 'ReprojectionMethod'], + '0x1006': ['CS', '1', 'SUVType'], + '0x1100': ['CS', '1', 'RandomsCorrectionMethod'], + '0x1101': ['LO', '1', 'AttenuationCorrectionMethod'], + '0x1102': ['CS', '1', 'DecayCorrection'], + '0x1103': ['LO', '1', 'ReconstructionMethod'], + '0x1104': ['LO', '1', 'DetectorLinesOfResponseUsed'], + '0x1105': ['LO', '1', 'ScatterCorrectionMethod'], + '0x1200': ['DS', '1', 'AxialAcceptance'], + '0x1201': ['IS', '2', 'AxialMash'], + '0x1202': ['IS', '1', 'TransverseMash'], + '0x1203': ['DS', '2', 'DetectorElementSize'], + '0x1210': ['DS', '1', 'CoincidenceWindowWidth'], + '0x1220': ['CS', '1-n', 'SecondaryCountsType'], + '0x1300': ['DS', '1', 'FrameReferenceTime'], + '0x1310': ['IS', '1', 'PrimaryPromptsCountsAccumulated'], + '0x1311': ['IS', '1-n', 'SecondaryCountsAccumulated'], + '0x1320': ['DS', '1', 'SliceSensitivityFactor'], + '0x1321': ['DS', '1', 'DecayFactor'], + '0x1322': ['DS', '1', 'DoseCalibrationFactor'], + '0x1323': ['DS', '1', 'ScatterFractionFactor'], + '0x1324': ['DS', '1', 'DeadTimeFactor'], + '0x1330': ['US', '1', 'ImageIndex'], + '0x1400': ['CS', '1-n', 'CountsIncluded'], + '0x1401': ['CS', '1', 'DeadTimeCorrectionFlag'] + }, + '0x0060': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x3000': ['SQ', '1', 'HistogramSequence'], + '0x3002': ['US', '1', 'HistogramNumberOfBins'], + '0x3004': ['xs', '1', 'HistogramFirstBinValue'], + '0x3006': ['xs', '1', 'HistogramLastBinValue'], + '0x3008': ['US', '1', 'HistogramBinWidth'], + '0x3010': ['LO', '1', 'HistogramExplanation'], + '0x3020': ['UL', '1-n', 'HistogramData'] + }, + '0x0062': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['CS', '1', 'SegmentationType'], + '0x0002': ['SQ', '1', 'SegmentSequence'], + '0x0003': ['SQ', '1', 'SegmentedPropertyCategoryCodeSequence'], + '0x0004': ['US', '1', 'SegmentNumber'], + '0x0005': ['LO', '1', 'SegmentLabel'], + '0x0006': ['ST', '1', 'SegmentDescription'], + '0x0008': ['CS', '1', 'SegmentAlgorithmType'], + '0x0009': ['LO', '1', 'SegmentAlgorithmName'], + '0x000A': ['SQ', '1', 'SegmentIdentificationSequence'], + '0x000B': ['US', '1-n', 'ReferencedSegmentNumber'], + '0x000C': ['US', '1', 'RecommendedDisplayGrayscaleValue'], + '0x000D': ['US', '3', 'RecommendedDisplayCIELabValue'], + '0x000E': ['US', '1', 'MaximumFractionalValue'], + '0x000F': ['SQ', '1', 'SegmentedPropertyTypeCodeSequence'], + '0x0010': ['CS', '1', 'SegmentationFractionalType'], + '0x0011': ['SQ', '1', 'SegmentedPropertyTypeModifierCodeSequence'], + '0x0012': ['SQ', '1', 'UsedSegmentsSequence'] + }, + '0x0064': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SQ', '1', 'DeformableRegistrationSequence'], + '0x0003': ['UI', '1', 'SourceFrameOfReferenceUID'], + '0x0005': ['SQ', '1', 'DeformableRegistrationGridSequence'], + '0x0007': ['UL', '3', 'GridDimensions'], + '0x0008': ['FD', '3', 'GridResolution'], + '0x0009': ['OF', '1', 'VectorGridData'], + '0x000F': ['SQ', '1', 'PreDeformationMatrixRegistrationSequence'], + '0x0010': ['SQ', '1', 'PostDeformationMatrixRegistrationSequence'] + }, + '0x0066': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['UL', '1', 'NumberOfSurfaces'], + '0x0002': ['SQ', '1', 'SurfaceSequence'], + '0x0003': ['UL', '1', 'SurfaceNumber'], + '0x0004': ['LT', '1', 'SurfaceComments'], + '0x0009': ['CS', '1', 'SurfaceProcessing'], + '0x000A': ['FL', '1', 'SurfaceProcessingRatio'], + '0x000B': ['LO', '1', 'SurfaceProcessingDescription'], + '0x000C': ['FL', '1', 'RecommendedPresentationOpacity'], + '0x000D': ['CS', '1', 'RecommendedPresentationType'], + '0x000E': ['CS', '1', 'FiniteVolume'], + '0x0010': ['CS', '1', 'Manifold'], + '0x0011': ['SQ', '1', 'SurfacePointsSequence'], + '0x0012': ['SQ', '1', 'SurfacePointsNormalsSequence'], + '0x0013': ['SQ', '1', 'SurfaceMeshPrimitivesSequence'], + '0x0015': ['UL', '1', 'NumberOfSurfacePoints'], + '0x0016': ['OF', '1', 'PointCoordinatesData'], + '0x0017': ['FL', '3', 'PointPositionAccuracy'], + '0x0018': ['FL', '1', 'MeanPointDistance'], + '0x0019': ['FL', '1', 'MaximumPointDistance'], + '0x001A': ['FL', '6', 'PointsBoundingBoxCoordinates'], + '0x001B': ['FL', '3', 'AxisOfRotation'], + '0x001C': ['FL', '3', 'CenterOfRotation'], + '0x001E': ['UL', '1', 'NumberOfVectors'], + '0x001F': ['US', '1', 'VectorDimensionality'], + '0x0020': ['FL', '1-n', 'VectorAccuracy'], + '0x0021': ['OF', '1', 'VectorCoordinateData'], + '0x0023': ['OW', '1', 'TrianglePointIndexList'], + '0x0024': ['OW', '1', 'EdgePointIndexList'], + '0x0025': ['OW', '1', 'VertexPointIndexList'], + '0x0026': ['SQ', '1', 'TriangleStripSequence'], + '0x0027': ['SQ', '1', 'TriangleFanSequence'], + '0x0028': ['SQ', '1', 'LineSequence'], + '0x0029': ['OW', '1', 'PrimitivePointIndexList'], + '0x002A': ['UL', '1', 'SurfaceCount'], + '0x002B': ['SQ', '1', 'ReferencedSurfaceSequence'], + '0x002C': ['UL', '1', 'ReferencedSurfaceNumber'], + '0x002D': ['SQ', '1', 'SegmentSurfaceGenerationAlgorithmIdentificationSequence'], + '0x002E': ['SQ', '1', 'SegmentSurfaceSourceInstanceSequence'], + '0x002F': ['SQ', '1', 'AlgorithmFamilyCodeSequence'], + '0x0030': ['SQ', '1', 'AlgorithmNameCodeSequence'], + '0x0031': ['LO', '1', 'AlgorithmVersion'], + '0x0032': ['LT', '1', 'AlgorithmParameters'], + '0x0034': ['SQ', '1', 'FacetSequence'], + '0x0035': ['SQ', '1', 'SurfaceProcessingAlgorithmIdentificationSequence'], + '0x0036': ['LO', '1', 'AlgorithmName'], + '0x0037': ['FL', '1', 'RecommendedPointRadius'], + '0x0038': ['FL', '1', 'RecommendedLineThickness'], + '0x0040': ['UL', '1-n', 'LongPrimitivePointIndexList'], + '0x0041': ['UL', '3-3n', 'LongTrianglePointIndexList'], + '0x0042': ['UL', '2-2n', 'LongEdgePointIndexList'], + '0x0043': ['UL', '1-n', 'LongVertexPointIndexList'] + }, + '0x0068': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x6210': ['LO', '1', 'ImplantSize'], + '0x6221': ['LO', '1', 'ImplantTemplateVersion'], + '0x6222': ['SQ', '1', 'ReplacedImplantTemplateSequence'], + '0x6223': ['CS', '1', 'ImplantType'], + '0x6224': ['SQ', '1', 'DerivationImplantTemplateSequence'], + '0x6225': ['SQ', '1', 'OriginalImplantTemplateSequence'], + '0x6226': ['DT', '1', 'EffectiveDateTime'], + '0x6230': ['SQ', '1', 'ImplantTargetAnatomySequence'], + '0x6260': ['SQ', '1', 'InformationFromManufacturerSequence'], + '0x6265': ['SQ', '1', 'NotificationFromManufacturerSequence'], + '0x6270': ['DT', '1', 'InformationIssueDateTime'], + '0x6280': ['ST', '1', 'InformationSummary'], + '0x62A0': ['SQ', '1', 'ImplantRegulatoryDisapprovalCodeSequence'], + '0x62A5': ['FD', '1', 'OverallTemplateSpatialTolerance'], + '0x62C0': ['SQ', '1', 'HPGLDocumentSequence'], + '0x62D0': ['US', '1', 'HPGLDocumentID'], + '0x62D5': ['LO', '1', 'HPGLDocumentLabel'], + '0x62E0': ['SQ', '1', 'ViewOrientationCodeSequence'], + '0x62F0': ['FD', '9', 'ViewOrientationModifier'], + '0x62F2': ['FD', '1', 'HPGLDocumentScaling'], + '0x6300': ['OB', '1', 'HPGLDocument'], + '0x6310': ['US', '1', 'HPGLContourPenNumber'], + '0x6320': ['SQ', '1', 'HPGLPenSequence'], + '0x6330': ['US', '1', 'HPGLPenNumber'], + '0x6340': ['LO', '1', 'HPGLPenLabel'], + '0x6345': ['ST', '1', 'HPGLPenDescription'], + '0x6346': ['FD', '2', 'RecommendedRotationPoint'], + '0x6347': ['FD', '4', 'BoundingRectangle'], + '0x6350': ['US', '1-n', 'ImplantTemplate3DModelSurfaceNumber'], + '0x6360': ['SQ', '1', 'SurfaceModelDescriptionSequence'], + '0x6380': ['LO', '1', 'SurfaceModelLabel'], + '0x6390': ['FD', '1', 'SurfaceModelScalingFactor'], + '0x63A0': ['SQ', '1', 'MaterialsCodeSequence'], + '0x63A4': ['SQ', '1', 'CoatingMaterialsCodeSequence'], + '0x63A8': ['SQ', '1', 'ImplantTypeCodeSequence'], + '0x63AC': ['SQ', '1', 'FixationMethodCodeSequence'], + '0x63B0': ['SQ', '1', 'MatingFeatureSetsSequence'], + '0x63C0': ['US', '1', 'MatingFeatureSetID'], + '0x63D0': ['LO', '1', 'MatingFeatureSetLabel'], + '0x63E0': ['SQ', '1', 'MatingFeatureSequence'], + '0x63F0': ['US', '1', 'MatingFeatureID'], + '0x6400': ['SQ', '1', 'MatingFeatureDegreeOfFreedomSequence'], + '0x6410': ['US', '1', 'DegreeOfFreedomID'], + '0x6420': ['CS', '1', 'DegreeOfFreedomType'], + '0x6430': ['SQ', '1', 'TwoDMatingFeatureCoordinatesSequence'], + '0x6440': ['US', '1', 'ReferencedHPGLDocumentID'], + '0x6450': ['FD', '2', 'TwoDMatingPoint'], + '0x6460': ['FD', '4', 'TwoDMatingAxes'], + '0x6470': ['SQ', '1', 'TwoDDegreeOfFreedomSequence'], + '0x6490': ['FD', '3', 'ThreeDDegreeOfFreedomAxis'], + '0x64A0': ['FD', '2', 'RangeOfFreedom'], + '0x64C0': ['FD', '3', 'ThreeDMatingPoint'], + '0x64D0': ['FD', '9', 'ThreeDMatingAxes'], + '0x64F0': ['FD', '3', 'TwoDDegreeOfFreedomAxis'], + '0x6500': ['SQ', '1', 'PlanningLandmarkPointSequence'], + '0x6510': ['SQ', '1', 'PlanningLandmarkLineSequence'], + '0x6520': ['SQ', '1', 'PlanningLandmarkPlaneSequence'], + '0x6530': ['US', '1', 'PlanningLandmarkID'], + '0x6540': ['LO', '1', 'PlanningLandmarkDescription'], + '0x6545': ['SQ', '1', 'PlanningLandmarkIdentificationCodeSequence'], + '0x6550': ['SQ', '1', 'TwoDPointCoordinatesSequence'], + '0x6560': ['FD', '2', 'TwoDPointCoordinates'], + '0x6590': ['FD', '3', 'ThreeDPointCoordinates'], + '0x65A0': ['SQ', '1', 'TwoDLineCoordinatesSequence'], + '0x65B0': ['FD', '4', 'TwoDLineCoordinates'], + '0x65D0': ['FD', '6', 'ThreeDLineCoordinates'], + '0x65E0': ['SQ', '1', 'TwoDPlaneCoordinatesSequence'], + '0x65F0': ['FD', '4', 'TwoDPlaneIntersection'], + '0x6610': ['FD', '3', 'ThreeDPlaneOrigin'], + '0x6620': ['FD', '3', 'ThreeDPlaneNormal'] + }, + '0x0070': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['SQ', '1', 'GraphicAnnotationSequence'], + '0x0002': ['CS', '1', 'GraphicLayer'], + '0x0003': ['CS', '1', 'BoundingBoxAnnotationUnits'], + '0x0004': ['CS', '1', 'AnchorPointAnnotationUnits'], + '0x0005': ['CS', '1', 'GraphicAnnotationUnits'], + '0x0006': ['ST', '1', 'UnformattedTextValue'], + '0x0008': ['SQ', '1', 'TextObjectSequence'], + '0x0009': ['SQ', '1', 'GraphicObjectSequence'], + '0x0010': ['FL', '2', 'BoundingBoxTopLeftHandCorner'], + '0x0011': ['FL', '2', 'BoundingBoxBottomRightHandCorner'], + '0x0012': ['CS', '1', 'BoundingBoxTextHorizontalJustification'], + '0x0014': ['FL', '2', 'AnchorPoint'], + '0x0015': ['CS', '1', 'AnchorPointVisibility'], + '0x0020': ['US', '1', 'GraphicDimensions'], + '0x0021': ['US', '1', 'NumberOfGraphicPoints'], + '0x0022': ['FL', '2-n', 'GraphicData'], + '0x0023': ['CS', '1', 'GraphicType'], + '0x0024': ['CS', '1', 'GraphicFilled'], + '0x0040': ['IS', '1', 'ImageRotationRetired'], + '0x0041': ['CS', '1', 'ImageHorizontalFlip'], + '0x0042': ['US', '1', 'ImageRotation'], + '0x0050': ['US', '2', 'DisplayedAreaTopLeftHandCornerTrial'], + '0x0051': ['US', '2', 'DisplayedAreaBottomRightHandCornerTrial'], + '0x0052': ['SL', '2', 'DisplayedAreaTopLeftHandCorner'], + '0x0053': ['SL', '2', 'DisplayedAreaBottomRightHandCorner'], + '0x005A': ['SQ', '1', 'DisplayedAreaSelectionSequence'], + '0x0060': ['SQ', '1', 'GraphicLayerSequence'], + '0x0062': ['IS', '1', 'GraphicLayerOrder'], + '0x0066': ['US', '1', 'GraphicLayerRecommendedDisplayGrayscaleValue'], + '0x0067': ['US', '3', 'GraphicLayerRecommendedDisplayRGBValue'], + '0x0068': ['LO', '1', 'GraphicLayerDescription'], + '0x0080': ['CS', '1', 'ContentLabel'], + '0x0081': ['LO', '1', 'ContentDescription'], + '0x0082': ['DA', '1', 'PresentationCreationDate'], + '0x0083': ['TM', '1', 'PresentationCreationTime'], + '0x0084': ['PN', '1', 'ContentCreatorName'], + '0x0086': ['SQ', '1', 'ContentCreatorIdentificationCodeSequence'], + '0x0087': ['SQ', '1', 'AlternateContentDescriptionSequence'], + '0x0100': ['CS', '1', 'PresentationSizeMode'], + '0x0101': ['DS', '2', 'PresentationPixelSpacing'], + '0x0102': ['IS', '2', 'PresentationPixelAspectRatio'], + '0x0103': ['FL', '1', 'PresentationPixelMagnificationRatio'], + '0x0207': ['LO', '1', 'GraphicGroupLabel'], + '0x0208': ['ST', '1', 'GraphicGroupDescription'], + '0x0209': ['SQ', '1', 'CompoundGraphicSequence'], + '0x0226': ['UL', '1', 'CompoundGraphicInstanceID'], + '0x0227': ['LO', '1', 'FontName'], + '0x0228': ['CS', '1', 'FontNameType'], + '0x0229': ['LO', '1', 'CSSFontName'], + '0x0230': ['FD', '1', 'RotationAngle'], + '0x0231': ['SQ', '1', 'TextStyleSequence'], + '0x0232': ['SQ', '1', 'LineStyleSequence'], + '0x0233': ['SQ', '1', 'FillStyleSequence'], + '0x0234': ['SQ', '1', 'GraphicGroupSequence'], + '0x0241': ['US', '3', 'TextColorCIELabValue'], + '0x0242': ['CS', '1', 'HorizontalAlignment'], + '0x0243': ['CS', '1', 'VerticalAlignment'], + '0x0244': ['CS', '1', 'ShadowStyle'], + '0x0245': ['FL', '1', 'ShadowOffsetX'], + '0x0246': ['FL', '1', 'ShadowOffsetY'], + '0x0247': ['US', '3', 'ShadowColorCIELabValue'], + '0x0248': ['CS', '1', 'Underlined'], + '0x0249': ['CS', '1', 'Bold'], + '0x0250': ['CS', '1', 'Italic'], + '0x0251': ['US', '3', 'PatternOnColorCIELabValue'], + '0x0252': ['US', '3', 'PatternOffColorCIELabValue'], + '0x0253': ['FL', '1', 'LineThickness'], + '0x0254': ['CS', '1', 'LineDashingStyle'], + '0x0255': ['UL', '1', 'LinePattern'], + '0x0256': ['OB', '1', 'FillPattern'], + '0x0257': ['CS', '1', 'FillMode'], + '0x0258': ['FL', '1', 'ShadowOpacity'], + '0x0261': ['FL', '1', 'GapLength'], + '0x0262': ['FL', '1', 'DiameterOfVisibility'], + '0x0273': ['FL', '2', 'RotationPoint'], + '0x0274': ['CS', '1', 'TickAlignment'], + '0x0278': ['CS', '1', 'ShowTickLabel'], + '0x0279': ['CS', '1', 'TickLabelAlignment'], + '0x0282': ['CS', '1', 'CompoundGraphicUnits'], + '0x0284': ['FL', '1', 'PatternOnOpacity'], + '0x0285': ['FL', '1', 'PatternOffOpacity'], + '0x0287': ['SQ', '1', 'MajorTicksSequence'], + '0x0288': ['FL', '1', 'TickPosition'], + '0x0289': ['SH', '1', 'TickLabel'], + '0x0294': ['CS', '1', 'CompoundGraphicType'], + '0x0295': ['UL', '1', 'GraphicGroupID'], + '0x0306': ['CS', '1', 'ShapeType'], + '0x0308': ['SQ', '1', 'RegistrationSequence'], + '0x0309': ['SQ', '1', 'MatrixRegistrationSequence'], + '0x030A': ['SQ', '1', 'MatrixSequence'], + '0x030C': ['CS', '1', 'FrameOfReferenceTransformationMatrixType'], + '0x030D': ['SQ', '1', 'RegistrationTypeCodeSequence'], + '0x030F': ['ST', '1', 'FiducialDescription'], + '0x0310': ['SH', '1', 'FiducialIdentifier'], + '0x0311': ['SQ', '1', 'FiducialIdentifierCodeSequence'], + '0x0312': ['FD', '1', 'ContourUncertaintyRadius'], + '0x0314': ['SQ', '1', 'UsedFiducialsSequence'], + '0x0318': ['SQ', '1', 'GraphicCoordinatesDataSequence'], + '0x031A': ['UI', '1', 'FiducialUID'], + '0x031C': ['SQ', '1', 'FiducialSetSequence'], + '0x031E': ['SQ', '1', 'FiducialSequence'], + '0x0401': ['US', '3', 'GraphicLayerRecommendedDisplayCIELabValue'], + '0x0402': ['SQ', '1', 'BlendingSequence'], + '0x0403': ['FL', '1', 'RelativeOpacity'], + '0x0404': ['SQ', '1', 'ReferencedSpatialRegistrationSequence'], + '0x0405': ['CS', '1', 'BlendingPosition'] + }, + '0x0072': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SH', '1', 'HangingProtocolName'], + '0x0004': ['LO', '1', 'HangingProtocolDescription'], + '0x0006': ['CS', '1', 'HangingProtocolLevel'], + '0x0008': ['LO', '1', 'HangingProtocolCreator'], + '0x000A': ['DT', '1', 'HangingProtocolCreationDateTime'], + '0x000C': ['SQ', '1', 'HangingProtocolDefinitionSequence'], + '0x000E': ['SQ', '1', 'HangingProtocolUserIdentificationCodeSequence'], + '0x0010': ['LO', '1', 'HangingProtocolUserGroupName'], + '0x0012': ['SQ', '1', 'SourceHangingProtocolSequence'], + '0x0014': ['US', '1', 'NumberOfPriorsReferenced'], + '0x0020': ['SQ', '1', 'ImageSetsSequence'], + '0x0022': ['SQ', '1', 'ImageSetSelectorSequence'], + '0x0024': ['CS', '1', 'ImageSetSelectorUsageFlag'], + '0x0026': ['AT', '1', 'SelectorAttribute'], + '0x0028': ['US', '1', 'SelectorValueNumber'], + '0x0030': ['SQ', '1', 'TimeBasedImageSetsSequence'], + '0x0032': ['US', '1', 'ImageSetNumber'], + '0x0034': ['CS', '1', 'ImageSetSelectorCategory'], + '0x0038': ['US', '2', 'RelativeTime'], + '0x003A': ['CS', '1', 'RelativeTimeUnits'], + '0x003C': ['SS', '2', 'AbstractPriorValue'], + '0x003E': ['SQ', '1', 'AbstractPriorCodeSequence'], + '0x0040': ['LO', '1', 'ImageSetLabel'], + '0x0050': ['CS', '1', 'SelectorAttributeVR'], + '0x0052': ['AT', '1-n', 'SelectorSequencePointer'], + '0x0054': ['LO', '1-n', 'SelectorSequencePointerPrivateCreator'], + '0x0056': ['LO', '1', 'SelectorAttributePrivateCreator'], + '0x0060': ['AT', '1-n', 'SelectorATValue'], + '0x0062': ['CS', '1-n', 'SelectorCSValue'], + '0x0064': ['IS', '1-n', 'SelectorISValue'], + '0x0066': ['LO', '1-n', 'SelectorLOValue'], + '0x0068': ['LT', '1', 'SelectorLTValue'], + '0x006A': ['PN', '1-n', 'SelectorPNValue'], + '0x006C': ['SH', '1-n', 'SelectorSHValue'], + '0x006E': ['ST', '1', 'SelectorSTValue'], + '0x0070': ['UT', '1', 'SelectorUTValue'], + '0x0072': ['DS', '1-n', 'SelectorDSValue'], + '0x0074': ['FD', '1-n', 'SelectorFDValue'], + '0x0076': ['FL', '1-n', 'SelectorFLValue'], + '0x0078': ['UL', '1-n', 'SelectorULValue'], + '0x007A': ['US', '1-n', 'SelectorUSValue'], + '0x007C': ['SL', '1-n', 'SelectorSLValue'], + '0x007E': ['SS', '1-n', 'SelectorSSValue'], + '0x007F': ['UI', '1-n', 'SelectorUIValue'], + '0x0080': ['SQ', '1', 'SelectorCodeSequenceValue'], + '0x0100': ['US', '1', 'NumberOfScreens'], + '0x0102': ['SQ', '1', 'NominalScreenDefinitionSequence'], + '0x0104': ['US', '1', 'NumberOfVerticalPixels'], + '0x0106': ['US', '1', 'NumberOfHorizontalPixels'], + '0x0108': ['FD', '4', 'DisplayEnvironmentSpatialPosition'], + '0x010A': ['US', '1', 'ScreenMinimumGrayscaleBitDepth'], + '0x010C': ['US', '1', 'ScreenMinimumColorBitDepth'], + '0x010E': ['US', '1', 'ApplicationMaximumRepaintTime'], + '0x0200': ['SQ', '1', 'DisplaySetsSequence'], + '0x0202': ['US', '1', 'DisplaySetNumber'], + '0x0203': ['LO', '1', 'DisplaySetLabel'], + '0x0204': ['US', '1', 'DisplaySetPresentationGroup'], + '0x0206': ['LO', '1', 'DisplaySetPresentationGroupDescription'], + '0x0208': ['CS', '1', 'PartialDataDisplayHandling'], + '0x0210': ['SQ', '1', 'SynchronizedScrollingSequence'], + '0x0212': ['US', '2-n', 'DisplaySetScrollingGroup'], + '0x0214': ['SQ', '1', 'NavigationIndicatorSequence'], + '0x0216': ['US', '1', 'NavigationDisplaySet'], + '0x0218': ['US', '1-n', 'ReferenceDisplaySets'], + '0x0300': ['SQ', '1', 'ImageBoxesSequence'], + '0x0302': ['US', '1', 'ImageBoxNumber'], + '0x0304': ['CS', '1', 'ImageBoxLayoutType'], + '0x0306': ['US', '1', 'ImageBoxTileHorizontalDimension'], + '0x0308': ['US', '1', 'ImageBoxTileVerticalDimension'], + '0x0310': ['CS', '1', 'ImageBoxScrollDirection'], + '0x0312': ['CS', '1', 'ImageBoxSmallScrollType'], + '0x0314': ['US', '1', 'ImageBoxSmallScrollAmount'], + '0x0316': ['CS', '1', 'ImageBoxLargeScrollType'], + '0x0318': ['US', '1', 'ImageBoxLargeScrollAmount'], + '0x0320': ['US', '1', 'ImageBoxOverlapPriority'], + '0x0330': ['FD', '1', 'CineRelativeToRealTime'], + '0x0400': ['SQ', '1', 'FilterOperationsSequence'], + '0x0402': ['CS', '1', 'FilterByCategory'], + '0x0404': ['CS', '1', 'FilterByAttributePresence'], + '0x0406': ['CS', '1', 'FilterByOperator'], + '0x0420': ['US', '3', 'StructuredDisplayBackgroundCIELabValue'], + '0x0421': ['US', '3', 'EmptyImageBoxCIELabValue'], + '0x0422': ['SQ', '1', 'StructuredDisplayImageBoxSequence'], + '0x0424': ['SQ', '1', 'StructuredDisplayTextBoxSequence'], + '0x0427': ['SQ', '1', 'ReferencedFirstFrameSequence'], + '0x0430': ['SQ', '1', 'ImageBoxSynchronizationSequence'], + '0x0432': ['US', '2-n', 'SynchronizedImageBoxList'], + '0x0434': ['CS', '1', 'TypeOfSynchronization'], + '0x0500': ['CS', '1', 'BlendingOperationType'], + '0x0510': ['CS', '1', 'ReformattingOperationType'], + '0x0512': ['FD', '1', 'ReformattingThickness'], + '0x0514': ['FD', '1', 'ReformattingInterval'], + '0x0516': ['CS', '1', 'ReformattingOperationInitialViewDirection'], + '0x0520': ['CS', '1-n', 'ThreeDRenderingType'], + '0x0600': ['SQ', '1', 'SortingOperationsSequence'], + '0x0602': ['CS', '1', 'SortByCategory'], + '0x0604': ['CS', '1', 'SortingDirection'], + '0x0700': ['CS', '2', 'DisplaySetPatientOrientation'], + '0x0702': ['CS', '1', 'VOIType'], + '0x0704': ['CS', '1', 'PseudoColorType'], + '0x0705': ['SQ', '1', 'PseudoColorPaletteInstanceReferenceSequence'], + '0x0706': ['CS', '1', 'ShowGrayscaleInverted'], + '0x0710': ['CS', '1', 'ShowImageTrueSizeFlag'], + '0x0712': ['CS', '1', 'ShowGraphicAnnotationFlag'], + '0x0714': ['CS', '1', 'ShowPatientDemographicsFlag'], + '0x0716': ['CS', '1', 'ShowAcquisitionTechniquesFlag'], + '0x0717': ['CS', '1', 'DisplaySetHorizontalJustification'], + '0x0718': ['CS', '1', 'DisplaySetVerticalJustification'] + }, + '0x0074': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0120': ['FD', '1', 'ContinuationStartMeterset'], + '0x0121': ['FD', '1', 'ContinuationEndMeterset'], + '0x1000': ['CS', '1', 'ProcedureStepState'], + '0x1002': ['SQ', '1', 'ProcedureStepProgressInformationSequence'], + '0x1004': ['DS', '1', 'ProcedureStepProgress'], + '0x1006': ['ST', '1', 'ProcedureStepProgressDescription'], + '0x1008': ['SQ', '1', 'ProcedureStepCommunicationsURISequence'], + '0x100A': ['UR', '1', 'ContactURI'], + '0x100C': ['LO', '1', 'ContactDisplayName'], + '0x100E': ['SQ', '1', 'ProcedureStepDiscontinuationReasonCodeSequence'], + '0x1020': ['SQ', '1', 'BeamTaskSequence'], + '0x1022': ['CS', '1', 'BeamTaskType'], + '0x1024': ['IS', '1', 'BeamOrderIndexTrial'], + '0x1025': ['CS', '1', 'AutosequenceFlag'], + '0x1026': ['FD', '1', 'TableTopVerticalAdjustedPosition'], + '0x1027': ['FD', '1', 'TableTopLongitudinalAdjustedPosition'], + '0x1028': ['FD', '1', 'TableTopLateralAdjustedPosition'], + '0x102A': ['FD', '1', 'PatientSupportAdjustedAngle'], + '0x102B': ['FD', '1', 'TableTopEccentricAdjustedAngle'], + '0x102C': ['FD', '1', 'TableTopPitchAdjustedAngle'], + '0x102D': ['FD', '1', 'TableTopRollAdjustedAngle'], + '0x1030': ['SQ', '1', 'DeliveryVerificationImageSequence'], + '0x1032': ['CS', '1', 'VerificationImageTiming'], + '0x1034': ['CS', '1', 'DoubleExposureFlag'], + '0x1036': ['CS', '1', 'DoubleExposureOrdering'], + '0x1038': ['DS', '1', 'DoubleExposureMetersetTrial'], + '0x103A': ['DS', '4', 'DoubleExposureFieldDeltaTrial'], + '0x1040': ['SQ', '1', 'RelatedReferenceRTImageSequence'], + '0x1042': ['SQ', '1', 'GeneralMachineVerificationSequence'], + '0x1044': ['SQ', '1', 'ConventionalMachineVerificationSequence'], + '0x1046': ['SQ', '1', 'IonMachineVerificationSequence'], + '0x1048': ['SQ', '1', 'FailedAttributesSequence'], + '0x104A': ['SQ', '1', 'OverriddenAttributesSequence'], + '0x104C': ['SQ', '1', 'ConventionalControlPointVerificationSequence'], + '0x104E': ['SQ', '1', 'IonControlPointVerificationSequence'], + '0x1050': ['SQ', '1', 'AttributeOccurrenceSequence'], + '0x1052': ['AT', '1', 'AttributeOccurrencePointer'], + '0x1054': ['UL', '1', 'AttributeItemSelector'], + '0x1056': ['LO', '1', 'AttributeOccurrencePrivateCreator'], + '0x1057': ['IS', '1-n', 'SelectorSequencePointerItems'], + '0x1200': ['CS', '1', 'ScheduledProcedureStepPriority'], + '0x1202': ['LO', '1', 'WorklistLabel'], + '0x1204': ['LO', '1', 'ProcedureStepLabel'], + '0x1210': ['SQ', '1', 'ScheduledProcessingParametersSequence'], + '0x1212': ['SQ', '1', 'PerformedProcessingParametersSequence'], + '0x1216': ['SQ', '1', 'UnifiedProcedureStepPerformedProcedureSequence'], + '0x1220': ['SQ', '1', 'RelatedProcedureStepSequence'], + '0x1222': ['LO', '1', 'ProcedureStepRelationshipType'], + '0x1224': ['SQ', '1', 'ReplacedProcedureStepSequence'], + '0x1230': ['LO', '1', 'DeletionLock'], + '0x1234': ['AE', '1', 'ReceivingAE'], + '0x1236': ['AE', '1', 'RequestingAE'], + '0x1238': ['LT', '1', 'ReasonForCancellation'], + '0x1242': ['CS', '1', 'SCPStatus'], + '0x1244': ['CS', '1', 'SubscriptionListStatus'], + '0x1246': ['CS', '1', 'UnifiedProcedureStepListStatus'], + '0x1324': ['UL', '1', 'BeamOrderIndex'], + '0x1338': ['FD', '1', 'DoubleExposureMeterset'], + '0x133A': ['FD', '4', 'DoubleExposureFieldDelta'] + }, + '0x0076': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['LO', '1', 'ImplantAssemblyTemplateName'], + '0x0003': ['LO', '1', 'ImplantAssemblyTemplateIssuer'], + '0x0006': ['LO', '1', 'ImplantAssemblyTemplateVersion'], + '0x0008': ['SQ', '1', 'ReplacedImplantAssemblyTemplateSequence'], + '0x000A': ['CS', '1', 'ImplantAssemblyTemplateType'], + '0x000C': ['SQ', '1', 'OriginalImplantAssemblyTemplateSequence'], + '0x000E': ['SQ', '1', 'DerivationImplantAssemblyTemplateSequence'], + '0x0010': ['SQ', '1', 'ImplantAssemblyTemplateTargetAnatomySequence'], + '0x0020': ['SQ', '1', 'ProcedureTypeCodeSequence'], + '0x0030': ['LO', '1', 'SurgicalTechnique'], + '0x0032': ['SQ', '1', 'ComponentTypesSequence'], + '0x0034': ['CS', '1', 'ComponentTypeCodeSequence'], + '0x0036': ['CS', '1', 'ExclusiveComponentType'], + '0x0038': ['CS', '1', 'MandatoryComponentType'], + '0x0040': ['SQ', '1', 'ComponentSequence'], + '0x0055': ['US', '1', 'ComponentID'], + '0x0060': ['SQ', '1', 'ComponentAssemblySequence'], + '0x0070': ['US', '1', 'Component1ReferencedID'], + '0x0080': ['US', '1', 'Component1ReferencedMatingFeatureSetID'], + '0x0090': ['US', '1', 'Component1ReferencedMatingFeatureID'], + '0x00A0': ['US', '1', 'Component2ReferencedID'], + '0x00B0': ['US', '1', 'Component2ReferencedMatingFeatureSetID'], + '0x00C0': ['US', '1', 'Component2ReferencedMatingFeatureID'] + }, + '0x0078': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['LO', '1', 'ImplantTemplateGroupName'], + '0x0010': ['ST', '1', 'ImplantTemplateGroupDescription'], + '0x0020': ['LO', '1', 'ImplantTemplateGroupIssuer'], + '0x0024': ['LO', '1', 'ImplantTemplateGroupVersion'], + '0x0026': ['SQ', '1', 'ReplacedImplantTemplateGroupSequence'], + '0x0028': ['SQ', '1', 'ImplantTemplateGroupTargetAnatomySequence'], + '0x002A': ['SQ', '1', 'ImplantTemplateGroupMembersSequence'], + '0x002E': ['US', '1', 'ImplantTemplateGroupMemberID'], + '0x0050': ['FD', '3', 'ThreeDImplantTemplateGroupMemberMatchingPoint'], + '0x0060': ['FD', '9', 'ThreeDImplantTemplateGroupMemberMatchingAxes'], + '0x0070': ['SQ', '1', 'ImplantTemplateGroupMemberMatching2DCoordinatesSequence'], + '0x0090': ['FD', '2', 'TwoDImplantTemplateGroupMemberMatchingPoint'], + '0x00A0': ['FD', '4', 'TwoDImplantTemplateGroupMemberMatchingAxes'], + '0x00B0': ['SQ', '1', 'ImplantTemplateGroupVariationDimensionSequence'], + '0x00B2': ['LO', '1', 'ImplantTemplateGroupVariationDimensionName'], + '0x00B4': ['SQ', '1', 'ImplantTemplateGroupVariationDimensionRankSequence'], + '0x00B6': ['US', '1', 'ReferencedImplantTemplateGroupMemberID'], + '0x00B8': ['US', '1', 'ImplantTemplateGroupVariationDimensionRank'] + }, + '0x0080': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['SQ', '1', 'SurfaceScanAcquisitionTypeCodeSequence'], + '0x0002': ['SQ', '1', 'SurfaceScanModeCodeSequence'], + '0x0003': ['SQ', '1', 'RegistrationMethodCodeSequence'], + '0x0004': ['FD', '1', 'ShotDurationTime'], + '0x0005': ['FD', '1', 'ShotOffsetTime'], + '0x0006': ['US', '1-n', 'SurfacePointPresentationValueData'], + '0x0007': ['US', '3-3n', 'SurfacePointColorCIELabValueData'], + '0x0008': ['SQ', '1', 'UVMappingSequence'], + '0x0009': ['SH', '1', 'TextureLabel'], + '0x0010': ['OF', '1-n', 'UValueData'], + '0x0011': ['OF', '1-n', 'VValueData'], + '0x0012': ['SQ', '1', 'ReferencedTextureSequence'], + '0x0013': ['SQ', '1', 'ReferencedSurfaceDataSequence'] + }, + '0x0088': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0130': ['SH', '1', 'StorageMediaFileSetID'], + '0x0140': ['UI', '1', 'StorageMediaFileSetUID'], + '0x0200': ['SQ', '1', 'IconImageSequence'], + '0x0904': ['LO', '1', 'TopicTitle'], + '0x0906': ['ST', '1', 'TopicSubject'], + '0x0910': ['LO', '1', 'TopicAuthor'], + '0x0912': ['LO', '1-32', 'TopicKeywords'] + }, + '0x0100': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0410': ['CS', '1', 'SOPInstanceStatus'], + '0x0420': ['DT', '1', 'SOPAuthorizationDateTime'], + '0x0424': ['LT', '1', 'SOPAuthorizationComment'], + '0x0426': ['LO', '1', 'AuthorizationEquipmentCertificationNumber'] + }, + '0x0400': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0005': ['US', '1', 'MACIDNumber'], + '0x0010': ['UI', '1', 'MACCalculationTransferSyntaxUID'], + '0x0015': ['CS', '1', 'MACAlgorithm'], + '0x0020': ['AT', '1-n', 'DataElementsSigned'], + '0x0100': ['UI', '1', 'DigitalSignatureUID'], + '0x0105': ['DT', '1', 'DigitalSignatureDateTime'], + '0x0110': ['CS', '1', 'CertificateType'], + '0x0115': ['OB', '1', 'CertificateOfSigner'], + '0x0120': ['OB', '1', 'Signature'], + '0x0305': ['CS', '1', 'CertifiedTimestampType'], + '0x0310': ['OB', '1', 'CertifiedTimestamp'], + '0x0401': ['SQ', '1', 'DigitalSignaturePurposeCodeSequence'], + '0x0402': ['SQ', '1', 'ReferencedDigitalSignatureSequence'], + '0x0403': ['SQ', '1', 'ReferencedSOPInstanceMACSequence'], + '0x0404': ['OB', '1', 'MAC'], + '0x0500': ['SQ', '1', 'EncryptedAttributesSequence'], + '0x0510': ['UI', '1', 'EncryptedContentTransferSyntaxUID'], + '0x0520': ['OB', '1', 'EncryptedContent'], + '0x0550': ['SQ', '1', 'ModifiedAttributesSequence'], + '0x0561': ['SQ', '1', 'OriginalAttributesSequence'], + '0x0562': ['DT', '1', 'AttributeModificationDateTime'], + '0x0563': ['LO', '1', 'ModifyingSystem'], + '0x0564': ['LO', '1', 'SourceOfPreviousValues'], + '0x0565': ['CS', '1', 'ReasonForTheAttributeModification'] + }, + '0x1000': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['US', '3', 'EscapeTriplet'], + '0x0011': ['US', '3', 'RunLengthTriplet'], + '0x0012': ['US', '1', 'HuffmanTableSize'], + '0x0013': ['US', '3', 'HuffmanTableTriplet'], + '0x0014': ['US', '1', 'ShiftTableSize'], + '0x0015': ['US', '3', 'ShiftTableTriplet'] + }, + '0x1010': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0004': ['US', '1-n', 'ZonalMap'] + }, + '0x2000': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['IS', '1', 'NumberOfCopies'], + '0x001E': ['SQ', '1', 'PrinterConfigurationSequence'], + '0x0020': ['CS', '1', 'PrintPriority'], + '0x0030': ['CS', '1', 'MediumType'], + '0x0040': ['CS', '1', 'FilmDestination'], + '0x0050': ['LO', '1', 'FilmSessionLabel'], + '0x0060': ['IS', '1', 'MemoryAllocation'], + '0x0061': ['IS', '1', 'MaximumMemoryAllocation'], + '0x0062': ['CS', '1', 'ColorImagePrintingFlag'], + '0x0063': ['CS', '1', 'CollationFlag'], + '0x0065': ['CS', '1', 'AnnotationFlag'], + '0x0067': ['CS', '1', 'ImageOverlayFlag'], + '0x0069': ['CS', '1', 'PresentationLUTFlag'], + '0x006A': ['CS', '1', 'ImageBoxPresentationLUTFlag'], + '0x00A0': ['US', '1', 'MemoryBitDepth'], + '0x00A1': ['US', '1', 'PrintingBitDepth'], + '0x00A2': ['SQ', '1', 'MediaInstalledSequence'], + '0x00A4': ['SQ', '1', 'OtherMediaAvailableSequence'], + '0x00A8': ['SQ', '1', 'SupportedImageDisplayFormatsSequence'], + '0x0500': ['SQ', '1', 'ReferencedFilmBoxSequence'], + '0x0510': ['SQ', '1', 'ReferencedStoredPrintSequence'] + }, + '0x2010': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['ST', '1', 'ImageDisplayFormat'], + '0x0030': ['CS', '1', 'AnnotationDisplayFormatID'], + '0x0040': ['CS', '1', 'FilmOrientation'], + '0x0050': ['CS', '1', 'FilmSizeID'], + '0x0052': ['CS', '1', 'PrinterResolutionID'], + '0x0054': ['CS', '1', 'DefaultPrinterResolutionID'], + '0x0060': ['CS', '1', 'MagnificationType'], + '0x0080': ['CS', '1', 'SmoothingType'], + '0x00A6': ['CS', '1', 'DefaultMagnificationType'], + '0x00A7': ['CS', '1-n', 'OtherMagnificationTypesAvailable'], + '0x00A8': ['CS', '1', 'DefaultSmoothingType'], + '0x00A9': ['CS', '1-n', 'OtherSmoothingTypesAvailable'], + '0x0100': ['CS', '1', 'BorderDensity'], + '0x0110': ['CS', '1', 'EmptyImageDensity'], + '0x0120': ['US', '1', 'MinDensity'], + '0x0130': ['US', '1', 'MaxDensity'], + '0x0140': ['CS', '1', 'Trim'], + '0x0150': ['ST', '1', 'ConfigurationInformation'], + '0x0152': ['LT', '1', 'ConfigurationInformationDescription'], + '0x0154': ['IS', '1', 'MaximumCollatedFilms'], + '0x015E': ['US', '1', 'Illumination'], + '0x0160': ['US', '1', 'ReflectedAmbientLight'], + '0x0376': ['DS', '2', 'PrinterPixelSpacing'], + '0x0500': ['SQ', '1', 'ReferencedFilmSessionSequence'], + '0x0510': ['SQ', '1', 'ReferencedImageBoxSequence'], + '0x0520': ['SQ', '1', 'ReferencedBasicAnnotationBoxSequence'] + }, + '0x2020': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['US', '1', 'ImageBoxPosition'], + '0x0020': ['CS', '1', 'Polarity'], + '0x0030': ['DS', '1', 'RequestedImageSize'], + '0x0040': ['CS', '1', 'RequestedDecimateCropBehavior'], + '0x0050': ['CS', '1', 'RequestedResolutionID'], + '0x00A0': ['CS', '1', 'RequestedImageSizeFlag'], + '0x00A2': ['CS', '1', 'DecimateCropResult'], + '0x0110': ['SQ', '1', 'BasicGrayscaleImageSequence'], + '0x0111': ['SQ', '1', 'BasicColorImageSequence'], + '0x0130': ['SQ', '1', 'ReferencedImageOverlayBoxSequence'], + '0x0140': ['SQ', '1', 'ReferencedVOILUTBoxSequence'] + }, + '0x2030': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['US', '1', 'AnnotationPosition'], + '0x0020': ['LO', '1', 'TextString'] + }, + '0x2040': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['SQ', '1', 'ReferencedOverlayPlaneSequence'], + '0x0011': ['US', '1-99', 'ReferencedOverlayPlaneGroups'], + '0x0020': ['SQ', '1', 'OverlayPixelDataSequence'], + '0x0060': ['CS', '1', 'OverlayMagnificationType'], + '0x0070': ['CS', '1', 'OverlaySmoothingType'], + '0x0072': ['CS', '1', 'OverlayOrImageMagnification'], + '0x0074': ['US', '1', 'MagnifyToNumberOfColumns'], + '0x0080': ['CS', '1', 'OverlayForegroundDensity'], + '0x0082': ['CS', '1', 'OverlayBackgroundDensity'], + '0x0090': ['CS', '1', 'OverlayMode'], + '0x0100': ['CS', '1', 'ThresholdDensity'], + '0x0500': ['SQ', '1', 'ReferencedImageBoxSequenceRetired'] + }, + '0x2050': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['SQ', '1', 'PresentationLUTSequence'], + '0x0020': ['CS', '1', 'PresentationLUTShape'], + '0x0500': ['SQ', '1', 'ReferencedPresentationLUTSequence'] + }, + '0x2100': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['SH', '1', 'PrintJobID'], + '0x0020': ['CS', '1', 'ExecutionStatus'], + '0x0030': ['CS', '1', 'ExecutionStatusInfo'], + '0x0040': ['DA', '1', 'CreationDate'], + '0x0050': ['TM', '1', 'CreationTime'], + '0x0070': ['AE', '1', 'Originator'], + '0x0140': ['AE', '1', 'DestinationAE'], + '0x0160': ['SH', '1', 'OwnerID'], + '0x0170': ['IS', '1', 'NumberOfFilms'], + '0x0500': ['SQ', '1', 'ReferencedPrintJobSequencePullStoredPrint'] + }, + '0x2110': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['CS', '1', 'PrinterStatus'], + '0x0020': ['CS', '1', 'PrinterStatusInfo'], + '0x0030': ['LO', '1', 'PrinterName'], + '0x0099': ['SH', '1', 'PrintQueueID'] + }, + '0x2120': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['CS', '1', 'QueueStatus'], + '0x0050': ['SQ', '1', 'PrintJobDescriptionSequence'], + '0x0070': ['SQ', '1', 'ReferencedPrintJobSequence'] + }, + '0x2130': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['SQ', '1', 'PrintManagementCapabilitiesSequence'], + '0x0015': ['SQ', '1', 'PrinterCharacteristicsSequence'], + '0x0030': ['SQ', '1', 'FilmBoxContentSequence'], + '0x0040': ['SQ', '1', 'ImageBoxContentSequence'], + '0x0050': ['SQ', '1', 'AnnotationContentSequence'], + '0x0060': ['SQ', '1', 'ImageOverlayBoxContentSequence'], + '0x0080': ['SQ', '1', 'PresentationLUTContentSequence'], + '0x00A0': ['SQ', '1', 'ProposedStudySequence'], + '0x00C0': ['SQ', '1', 'OriginalImageSequence'] + }, + '0x2200': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['CS', '1', 'LabelUsingInformationExtractedFromInstances'], + '0x0002': ['UT', '1', 'LabelText'], + '0x0003': ['CS', '1', 'LabelStyleSelection'], + '0x0004': ['LT', '1', 'MediaDisposition'], + '0x0005': ['LT', '1', 'BarcodeValue'], + '0x0006': ['CS', '1', 'BarcodeSymbology'], + '0x0007': ['CS', '1', 'AllowMediaSplitting'], + '0x0008': ['CS', '1', 'IncludeNonDICOMObjects'], + '0x0009': ['CS', '1', 'IncludeDisplayApplication'], + '0x000A': ['CS', '1', 'PreserveCompositeInstancesAfterMediaCreation'], + '0x000B': ['US', '1', 'TotalNumberOfPiecesOfMediaCreated'], + '0x000C': ['LO', '1', 'RequestedMediaApplicationProfile'], + '0x000D': ['SQ', '1', 'ReferencedStorageMediaSequence'], + '0x000E': ['AT', '1-n', 'FailureAttributes'], + '0x000F': ['CS', '1', 'AllowLossyCompression'], + '0x0020': ['CS', '1', 'RequestPriority'] + }, + '0x3002': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SH', '1', 'RTImageLabel'], + '0x0003': ['LO', '1', 'RTImageName'], + '0x0004': ['ST', '1', 'RTImageDescription'], + '0x000A': ['CS', '1', 'ReportedValuesOrigin'], + '0x000C': ['CS', '1', 'RTImagePlane'], + '0x000D': ['DS', '3', 'XRayImageReceptorTranslation'], + '0x000E': ['DS', '1', 'XRayImageReceptorAngle'], + '0x0010': ['DS', '6', 'RTImageOrientation'], + '0x0011': ['DS', '2', 'ImagePlanePixelSpacing'], + '0x0012': ['DS', '2', 'RTImagePosition'], + '0x0020': ['SH', '1', 'RadiationMachineName'], + '0x0022': ['DS', '1', 'RadiationMachineSAD'], + '0x0024': ['DS', '1', 'RadiationMachineSSD'], + '0x0026': ['DS', '1', 'RTImageSID'], + '0x0028': ['DS', '1', 'SourceToReferenceObjectDistance'], + '0x0029': ['IS', '1', 'FractionNumber'], + '0x0030': ['SQ', '1', 'ExposureSequence'], + '0x0032': ['DS', '1', 'MetersetExposure'], + '0x0034': ['DS', '4', 'DiaphragmPosition'], + '0x0040': ['SQ', '1', 'FluenceMapSequence'], + '0x0041': ['CS', '1', 'FluenceDataSource'], + '0x0042': ['DS', '1', 'FluenceDataScale'], + '0x0050': ['SQ', '1', 'PrimaryFluenceModeSequence'], + '0x0051': ['CS', '1', 'FluenceMode'], + '0x0052': ['SH', '1', 'FluenceModeID'] + }, + '0x3004': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['CS', '1', 'DVHType'], + '0x0002': ['CS', '1', 'DoseUnits'], + '0x0004': ['CS', '1', 'DoseType'], + '0x0005': ['CS', '1', 'SpatialTransformOfDose'], + '0x0006': ['LO', '1', 'DoseComment'], + '0x0008': ['DS', '3', 'NormalizationPoint'], + '0x000A': ['CS', '1', 'DoseSummationType'], + '0x000C': ['DS', '2-n', 'GridFrameOffsetVector'], + '0x000E': ['DS', '1', 'DoseGridScaling'], + '0x0010': ['SQ', '1', 'RTDoseROISequence'], + '0x0012': ['DS', '1', 'DoseValue'], + '0x0014': ['CS', '1-3', 'TissueHeterogeneityCorrection'], + '0x0040': ['DS', '3', 'DVHNormalizationPoint'], + '0x0042': ['DS', '1', 'DVHNormalizationDoseValue'], + '0x0050': ['SQ', '1', 'DVHSequence'], + '0x0052': ['DS', '1', 'DVHDoseScaling'], + '0x0054': ['CS', '1', 'DVHVolumeUnits'], + '0x0056': ['IS', '1', 'DVHNumberOfBins'], + '0x0058': ['DS', '2-2n', 'DVHData'], + '0x0060': ['SQ', '1', 'DVHReferencedROISequence'], + '0x0062': ['CS', '1', 'DVHROIContributionType'], + '0x0070': ['DS', '1', 'DVHMinimumDose'], + '0x0072': ['DS', '1', 'DVHMaximumDose'], + '0x0074': ['DS', '1', 'DVHMeanDose'] + }, + '0x3006': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SH', '1', 'StructureSetLabel'], + '0x0004': ['LO', '1', 'StructureSetName'], + '0x0006': ['ST', '1', 'StructureSetDescription'], + '0x0008': ['DA', '1', 'StructureSetDate'], + '0x0009': ['TM', '1', 'StructureSetTime'], + '0x0010': ['SQ', '1', 'ReferencedFrameOfReferenceSequence'], + '0x0012': ['SQ', '1', 'RTReferencedStudySequence'], + '0x0014': ['SQ', '1', 'RTReferencedSeriesSequence'], + '0x0016': ['SQ', '1', 'ContourImageSequence'], + '0x0018': ['SQ', '1', 'PredecessorStructureSetSequence'], + '0x0020': ['SQ', '1', 'StructureSetROISequence'], + '0x0022': ['IS', '1', 'ROINumber'], + '0x0024': ['UI', '1', 'ReferencedFrameOfReferenceUID'], + '0x0026': ['LO', '1', 'ROIName'], + '0x0028': ['ST', '1', 'ROIDescription'], + '0x002A': ['IS', '3', 'ROIDisplayColor'], + '0x002C': ['DS', '1', 'ROIVolume'], + '0x0030': ['SQ', '1', 'RTRelatedROISequence'], + '0x0033': ['CS', '1', 'RTROIRelationship'], + '0x0036': ['CS', '1', 'ROIGenerationAlgorithm'], + '0x0038': ['LO', '1', 'ROIGenerationDescription'], + '0x0039': ['SQ', '1', 'ROIContourSequence'], + '0x0040': ['SQ', '1', 'ContourSequence'], + '0x0042': ['CS', '1', 'ContourGeometricType'], + '0x0044': ['DS', '1', 'ContourSlabThickness'], + '0x0045': ['DS', '3', 'ContourOffsetVector'], + '0x0046': ['IS', '1', 'NumberOfContourPoints'], + '0x0048': ['IS', '1', 'ContourNumber'], + '0x0049': ['IS', '1-n', 'AttachedContours'], + '0x0050': ['DS', '3-3n', 'ContourData'], + '0x0080': ['SQ', '1', 'RTROIObservationsSequence'], + '0x0082': ['IS', '1', 'ObservationNumber'], + '0x0084': ['IS', '1', 'ReferencedROINumber'], + '0x0085': ['SH', '1', 'ROIObservationLabel'], + '0x0086': ['SQ', '1', 'RTROIIdentificationCodeSequence'], + '0x0088': ['ST', '1', 'ROIObservationDescription'], + '0x00A0': ['SQ', '1', 'RelatedRTROIObservationsSequence'], + '0x00A4': ['CS', '1', 'RTROIInterpretedType'], + '0x00A6': ['PN', '1', 'ROIInterpreter'], + '0x00B0': ['SQ', '1', 'ROIPhysicalPropertiesSequence'], + '0x00B2': ['CS', '1', 'ROIPhysicalProperty'], + '0x00B4': ['DS', '1', 'ROIPhysicalPropertyValue'], + '0x00B6': ['SQ', '1', 'ROIElementalCompositionSequence'], + '0x00B7': ['US', '1', 'ROIElementalCompositionAtomicNumber'], + '0x00B8': ['FL', '1', 'ROIElementalCompositionAtomicMassFraction'], + '0x00B9': ['SQ', '1', 'AdditionalRTROIIdentificationCodeSequence'], + '0x00C0': ['SQ', '1', 'FrameOfReferenceRelationshipSequence'], + '0x00C2': ['UI', '1', 'RelatedFrameOfReferenceUID'], + '0x00C4': ['CS', '1', 'FrameOfReferenceTransformationType'], + '0x00C6': ['DS', '16', 'FrameOfReferenceTransformationMatrix'], + '0x00C8': ['LO', '1', 'FrameOfReferenceTransformationComment'] + }, + '0x3008': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['SQ', '1', 'MeasuredDoseReferenceSequence'], + '0x0012': ['ST', '1', 'MeasuredDoseDescription'], + '0x0014': ['CS', '1', 'MeasuredDoseType'], + '0x0016': ['DS', '1', 'MeasuredDoseValue'], + '0x0020': ['SQ', '1', 'TreatmentSessionBeamSequence'], + '0x0021': ['SQ', '1', 'TreatmentSessionIonBeamSequence'], + '0x0022': ['IS', '1', 'CurrentFractionNumber'], + '0x0024': ['DA', '1', 'TreatmentControlPointDate'], + '0x0025': ['TM', '1', 'TreatmentControlPointTime'], + '0x002A': ['CS', '1', 'TreatmentTerminationStatus'], + '0x002B': ['SH', '1', 'TreatmentTerminationCode'], + '0x002C': ['CS', '1', 'TreatmentVerificationStatus'], + '0x0030': ['SQ', '1', 'ReferencedTreatmentRecordSequence'], + '0x0032': ['DS', '1', 'SpecifiedPrimaryMeterset'], + '0x0033': ['DS', '1', 'SpecifiedSecondaryMeterset'], + '0x0036': ['DS', '1', 'DeliveredPrimaryMeterset'], + '0x0037': ['DS', '1', 'DeliveredSecondaryMeterset'], + '0x003A': ['DS', '1', 'SpecifiedTreatmentTime'], + '0x003B': ['DS', '1', 'DeliveredTreatmentTime'], + '0x0040': ['SQ', '1', 'ControlPointDeliverySequence'], + '0x0041': ['SQ', '1', 'IonControlPointDeliverySequence'], + '0x0042': ['DS', '1', 'SpecifiedMeterset'], + '0x0044': ['DS', '1', 'DeliveredMeterset'], + '0x0045': ['FL', '1', 'MetersetRateSet'], + '0x0046': ['FL', '1', 'MetersetRateDelivered'], + '0x0047': ['FL', '1-n', 'ScanSpotMetersetsDelivered'], + '0x0048': ['DS', '1', 'DoseRateDelivered'], + '0x0050': ['SQ', '1', 'TreatmentSummaryCalculatedDoseReferenceSequence'], + '0x0052': ['DS', '1', 'CumulativeDoseToDoseReference'], + '0x0054': ['DA', '1', 'FirstTreatmentDate'], + '0x0056': ['DA', '1', 'MostRecentTreatmentDate'], + '0x005A': ['IS', '1', 'NumberOfFractionsDelivered'], + '0x0060': ['SQ', '1', 'OverrideSequence'], + '0x0061': ['AT', '1', 'ParameterSequencePointer'], + '0x0062': ['AT', '1', 'OverrideParameterPointer'], + '0x0063': ['IS', '1', 'ParameterItemIndex'], + '0x0064': ['IS', '1', 'MeasuredDoseReferenceNumber'], + '0x0065': ['AT', '1', 'ParameterPointer'], + '0x0066': ['ST', '1', 'OverrideReason'], + '0x0068': ['SQ', '1', 'CorrectedParameterSequence'], + '0x006A': ['FL', '1', 'CorrectionValue'], + '0x0070': ['SQ', '1', 'CalculatedDoseReferenceSequence'], + '0x0072': ['IS', '1', 'CalculatedDoseReferenceNumber'], + '0x0074': ['ST', '1', 'CalculatedDoseReferenceDescription'], + '0x0076': ['DS', '1', 'CalculatedDoseReferenceDoseValue'], + '0x0078': ['DS', '1', 'StartMeterset'], + '0x007A': ['DS', '1', 'EndMeterset'], + '0x0080': ['SQ', '1', 'ReferencedMeasuredDoseReferenceSequence'], + '0x0082': ['IS', '1', 'ReferencedMeasuredDoseReferenceNumber'], + '0x0090': ['SQ', '1', 'ReferencedCalculatedDoseReferenceSequence'], + '0x0092': ['IS', '1', 'ReferencedCalculatedDoseReferenceNumber'], + '0x00A0': ['SQ', '1', 'BeamLimitingDeviceLeafPairsSequence'], + '0x00B0': ['SQ', '1', 'RecordedWedgeSequence'], + '0x00C0': ['SQ', '1', 'RecordedCompensatorSequence'], + '0x00D0': ['SQ', '1', 'RecordedBlockSequence'], + '0x00E0': ['SQ', '1', 'TreatmentSummaryMeasuredDoseReferenceSequence'], + '0x00F0': ['SQ', '1', 'RecordedSnoutSequence'], + '0x00F2': ['SQ', '1', 'RecordedRangeShifterSequence'], + '0x00F4': ['SQ', '1', 'RecordedLateralSpreadingDeviceSequence'], + '0x00F6': ['SQ', '1', 'RecordedRangeModulatorSequence'], + '0x0100': ['SQ', '1', 'RecordedSourceSequence'], + '0x0105': ['LO', '1', 'SourceSerialNumber'], + '0x0110': ['SQ', '1', 'TreatmentSessionApplicationSetupSequence'], + '0x0116': ['CS', '1', 'ApplicationSetupCheck'], + '0x0120': ['SQ', '1', 'RecordedBrachyAccessoryDeviceSequence'], + '0x0122': ['IS', '1', 'ReferencedBrachyAccessoryDeviceNumber'], + '0x0130': ['SQ', '1', 'RecordedChannelSequence'], + '0x0132': ['DS', '1', 'SpecifiedChannelTotalTime'], + '0x0134': ['DS', '1', 'DeliveredChannelTotalTime'], + '0x0136': ['IS', '1', 'SpecifiedNumberOfPulses'], + '0x0138': ['IS', '1', 'DeliveredNumberOfPulses'], + '0x013A': ['DS', '1', 'SpecifiedPulseRepetitionInterval'], + '0x013C': ['DS', '1', 'DeliveredPulseRepetitionInterval'], + '0x0140': ['SQ', '1', 'RecordedSourceApplicatorSequence'], + '0x0142': ['IS', '1', 'ReferencedSourceApplicatorNumber'], + '0x0150': ['SQ', '1', 'RecordedChannelShieldSequence'], + '0x0152': ['IS', '1', 'ReferencedChannelShieldNumber'], + '0x0160': ['SQ', '1', 'BrachyControlPointDeliveredSequence'], + '0x0162': ['DA', '1', 'SafePositionExitDate'], + '0x0164': ['TM', '1', 'SafePositionExitTime'], + '0x0166': ['DA', '1', 'SafePositionReturnDate'], + '0x0168': ['TM', '1', 'SafePositionReturnTime'], + '0x0171': ['SQ', '1', 'PulseSpecificBrachyControlPointDeliveredSequence'], + '0x0172': ['US', '1', 'PulseNumber'], + '0x0173': ['SQ', '1', 'BrachyPulseControlPointDeliveredSequence'], + '0x0200': ['CS', '1', 'CurrentTreatmentStatus'], + '0x0202': ['ST', '1', 'TreatmentStatusComment'], + '0x0220': ['SQ', '1', 'FractionGroupSummarySequence'], + '0x0223': ['IS', '1', 'ReferencedFractionNumber'], + '0x0224': ['CS', '1', 'FractionGroupType'], + '0x0230': ['CS', '1', 'BeamStopperPosition'], + '0x0240': ['SQ', '1', 'FractionStatusSummarySequence'], + '0x0250': ['DA', '1', 'TreatmentDate'], + '0x0251': ['TM', '1', 'TreatmentTime'] + }, + '0x300A': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SH', '1', 'RTPlanLabel'], + '0x0003': ['LO', '1', 'RTPlanName'], + '0x0004': ['ST', '1', 'RTPlanDescription'], + '0x0006': ['DA', '1', 'RTPlanDate'], + '0x0007': ['TM', '1', 'RTPlanTime'], + '0x0009': ['LO', '1-n', 'TreatmentProtocols'], + '0x000A': ['CS', '1', 'PlanIntent'], + '0x000B': ['LO', '1-n', 'TreatmentSites'], + '0x000C': ['CS', '1', 'RTPlanGeometry'], + '0x000E': ['ST', '1', 'PrescriptionDescription'], + '0x0010': ['SQ', '1', 'DoseReferenceSequence'], + '0x0012': ['IS', '1', 'DoseReferenceNumber'], + '0x0013': ['UI', '1', 'DoseReferenceUID'], + '0x0014': ['CS', '1', 'DoseReferenceStructureType'], + '0x0015': ['CS', '1', 'NominalBeamEnergyUnit'], + '0x0016': ['LO', '1', 'DoseReferenceDescription'], + '0x0018': ['DS', '3', 'DoseReferencePointCoordinates'], + '0x001A': ['DS', '1', 'NominalPriorDose'], + '0x0020': ['CS', '1', 'DoseReferenceType'], + '0x0021': ['DS', '1', 'ConstraintWeight'], + '0x0022': ['DS', '1', 'DeliveryWarningDose'], + '0x0023': ['DS', '1', 'DeliveryMaximumDose'], + '0x0025': ['DS', '1', 'TargetMinimumDose'], + '0x0026': ['DS', '1', 'TargetPrescriptionDose'], + '0x0027': ['DS', '1', 'TargetMaximumDose'], + '0x0028': ['DS', '1', 'TargetUnderdoseVolumeFraction'], + '0x002A': ['DS', '1', 'OrganAtRiskFullVolumeDose'], + '0x002B': ['DS', '1', 'OrganAtRiskLimitDose'], + '0x002C': ['DS', '1', 'OrganAtRiskMaximumDose'], + '0x002D': ['DS', '1', 'OrganAtRiskOverdoseVolumeFraction'], + '0x0040': ['SQ', '1', 'ToleranceTableSequence'], + '0x0042': ['IS', '1', 'ToleranceTableNumber'], + '0x0043': ['SH', '1', 'ToleranceTableLabel'], + '0x0044': ['DS', '1', 'GantryAngleTolerance'], + '0x0046': ['DS', '1', 'BeamLimitingDeviceAngleTolerance'], + '0x0048': ['SQ', '1', 'BeamLimitingDeviceToleranceSequence'], + '0x004A': ['DS', '1', 'BeamLimitingDevicePositionTolerance'], + '0x004B': ['FL', '1', 'SnoutPositionTolerance'], + '0x004C': ['DS', '1', 'PatientSupportAngleTolerance'], + '0x004E': ['DS', '1', 'TableTopEccentricAngleTolerance'], + '0x004F': ['FL', '1', 'TableTopPitchAngleTolerance'], + '0x0050': ['FL', '1', 'TableTopRollAngleTolerance'], + '0x0051': ['DS', '1', 'TableTopVerticalPositionTolerance'], + '0x0052': ['DS', '1', 'TableTopLongitudinalPositionTolerance'], + '0x0053': ['DS', '1', 'TableTopLateralPositionTolerance'], + '0x0055': ['CS', '1', 'RTPlanRelationship'], + '0x0070': ['SQ', '1', 'FractionGroupSequence'], + '0x0071': ['IS', '1', 'FractionGroupNumber'], + '0x0072': ['LO', '1', 'FractionGroupDescription'], + '0x0078': ['IS', '1', 'NumberOfFractionsPlanned'], + '0x0079': ['IS', '1', 'NumberOfFractionPatternDigitsPerDay'], + '0x007A': ['IS', '1', 'RepeatFractionCycleLength'], + '0x007B': ['LT', '1', 'FractionPattern'], + '0x0080': ['IS', '1', 'NumberOfBeams'], + '0x0082': ['DS', '3', 'BeamDoseSpecificationPoint'], + '0x0084': ['DS', '1', 'BeamDose'], + '0x0086': ['DS', '1', 'BeamMeterset'], + '0x0088': ['FL', '1', 'BeamDosePointDepth'], + '0x0089': ['FL', '1', 'BeamDosePointEquivalentDepth'], + '0x008A': ['FL', '1', 'BeamDosePointSSD'], + '0x008B': ['CS', '1', 'BeamDoseMeaning'], + '0x008C': ['SQ', '1', 'BeamDoseVerificationControlPointSequence'], + '0x008D': ['FL', '1', 'AverageBeamDosePointDepth'], + '0x008E': ['FL', '1', 'AverageBeamDosePointEquivalentDepth'], + '0x008F': ['FL', '1', 'AverageBeamDosePointSSD'], + '0x00A0': ['IS', '1', 'NumberOfBrachyApplicationSetups'], + '0x00A2': ['DS', '3', 'BrachyApplicationSetupDoseSpecificationPoint'], + '0x00A4': ['DS', '1', 'BrachyApplicationSetupDose'], + '0x00B0': ['SQ', '1', 'BeamSequence'], + '0x00B2': ['SH', '1', 'TreatmentMachineName'], + '0x00B3': ['CS', '1', 'PrimaryDosimeterUnit'], + '0x00B4': ['DS', '1', 'SourceAxisDistance'], + '0x00B6': ['SQ', '1', 'BeamLimitingDeviceSequence'], + '0x00B8': ['CS', '1', 'RTBeamLimitingDeviceType'], + '0x00BA': ['DS', '1', 'SourceToBeamLimitingDeviceDistance'], + '0x00BB': ['FL', '1', 'IsocenterToBeamLimitingDeviceDistance'], + '0x00BC': ['IS', '1', 'NumberOfLeafJawPairs'], + '0x00BE': ['DS', '3-n', 'LeafPositionBoundaries'], + '0x00C0': ['IS', '1', 'BeamNumber'], + '0x00C2': ['LO', '1', 'BeamName'], + '0x00C3': ['ST', '1', 'BeamDescription'], + '0x00C4': ['CS', '1', 'BeamType'], + '0x00C5': ['FD', '1', 'BeamDeliveryDurationLimit'], + '0x00C6': ['CS', '1', 'RadiationType'], + '0x00C7': ['CS', '1', 'HighDoseTechniqueType'], + '0x00C8': ['IS', '1', 'ReferenceImageNumber'], + '0x00CA': ['SQ', '1', 'PlannedVerificationImageSequence'], + '0x00CC': ['LO', '1-n', 'ImagingDeviceSpecificAcquisitionParameters'], + '0x00CE': ['CS', '1', 'TreatmentDeliveryType'], + '0x00D0': ['IS', '1', 'NumberOfWedges'], + '0x00D1': ['SQ', '1', 'WedgeSequence'], + '0x00D2': ['IS', '1', 'WedgeNumber'], + '0x00D3': ['CS', '1', 'WedgeType'], + '0x00D4': ['SH', '1', 'WedgeID'], + '0x00D5': ['IS', '1', 'WedgeAngle'], + '0x00D6': ['DS', '1', 'WedgeFactor'], + '0x00D7': ['FL', '1', 'TotalWedgeTrayWaterEquivalentThickness'], + '0x00D8': ['DS', '1', 'WedgeOrientation'], + '0x00D9': ['FL', '1', 'IsocenterToWedgeTrayDistance'], + '0x00DA': ['DS', '1', 'SourceToWedgeTrayDistance'], + '0x00DB': ['FL', '1', 'WedgeThinEdgePosition'], + '0x00DC': ['SH', '1', 'BolusID'], + '0x00DD': ['ST', '1', 'BolusDescription'], + '0x00DE': ['DS', '1', 'EffectiveWedgeAngle'], + '0x00E0': ['IS', '1', 'NumberOfCompensators'], + '0x00E1': ['SH', '1', 'MaterialID'], + '0x00E2': ['DS', '1', 'TotalCompensatorTrayFactor'], + '0x00E3': ['SQ', '1', 'CompensatorSequence'], + '0x00E4': ['IS', '1', 'CompensatorNumber'], + '0x00E5': ['SH', '1', 'CompensatorID'], + '0x00E6': ['DS', '1', 'SourceToCompensatorTrayDistance'], + '0x00E7': ['IS', '1', 'CompensatorRows'], + '0x00E8': ['IS', '1', 'CompensatorColumns'], + '0x00E9': ['DS', '2', 'CompensatorPixelSpacing'], + '0x00EA': ['DS', '2', 'CompensatorPosition'], + '0x00EB': ['DS', '1-n', 'CompensatorTransmissionData'], + '0x00EC': ['DS', '1-n', 'CompensatorThicknessData'], + '0x00ED': ['IS', '1', 'NumberOfBoli'], + '0x00EE': ['CS', '1', 'CompensatorType'], + '0x00EF': ['SH', '1', 'CompensatorTrayID'], + '0x00F0': ['IS', '1', 'NumberOfBlocks'], + '0x00F2': ['DS', '1', 'TotalBlockTrayFactor'], + '0x00F3': ['FL', '1', 'TotalBlockTrayWaterEquivalentThickness'], + '0x00F4': ['SQ', '1', 'BlockSequence'], + '0x00F5': ['SH', '1', 'BlockTrayID'], + '0x00F6': ['DS', '1', 'SourceToBlockTrayDistance'], + '0x00F7': ['FL', '1', 'IsocenterToBlockTrayDistance'], + '0x00F8': ['CS', '1', 'BlockType'], + '0x00F9': ['LO', '1', 'AccessoryCode'], + '0x00FA': ['CS', '1', 'BlockDivergence'], + '0x00FB': ['CS', '1', 'BlockMountingPosition'], + '0x00FC': ['IS', '1', 'BlockNumber'], + '0x00FE': ['LO', '1', 'BlockName'], + '0x0100': ['DS', '1', 'BlockThickness'], + '0x0102': ['DS', '1', 'BlockTransmission'], + '0x0104': ['IS', '1', 'BlockNumberOfPoints'], + '0x0106': ['DS', '2-2n', 'BlockData'], + '0x0107': ['SQ', '1', 'ApplicatorSequence'], + '0x0108': ['SH', '1', 'ApplicatorID'], + '0x0109': ['CS', '1', 'ApplicatorType'], + '0x010A': ['LO', '1', 'ApplicatorDescription'], + '0x010C': ['DS', '1', 'CumulativeDoseReferenceCoefficient'], + '0x010E': ['DS', '1', 'FinalCumulativeMetersetWeight'], + '0x0110': ['IS', '1', 'NumberOfControlPoints'], + '0x0111': ['SQ', '1', 'ControlPointSequence'], + '0x0112': ['IS', '1', 'ControlPointIndex'], + '0x0114': ['DS', '1', 'NominalBeamEnergy'], + '0x0115': ['DS', '1', 'DoseRateSet'], + '0x0116': ['SQ', '1', 'WedgePositionSequence'], + '0x0118': ['CS', '1', 'WedgePosition'], + '0x011A': ['SQ', '1', 'BeamLimitingDevicePositionSequence'], + '0x011C': ['DS', '2-2n', 'LeafJawPositions'], + '0x011E': ['DS', '1', 'GantryAngle'], + '0x011F': ['CS', '1', 'GantryRotationDirection'], + '0x0120': ['DS', '1', 'BeamLimitingDeviceAngle'], + '0x0121': ['CS', '1', 'BeamLimitingDeviceRotationDirection'], + '0x0122': ['DS', '1', 'PatientSupportAngle'], + '0x0123': ['CS', '1', 'PatientSupportRotationDirection'], + '0x0124': ['DS', '1', 'TableTopEccentricAxisDistance'], + '0x0125': ['DS', '1', 'TableTopEccentricAngle'], + '0x0126': ['CS', '1', 'TableTopEccentricRotationDirection'], + '0x0128': ['DS', '1', 'TableTopVerticalPosition'], + '0x0129': ['DS', '1', 'TableTopLongitudinalPosition'], + '0x012A': ['DS', '1', 'TableTopLateralPosition'], + '0x012C': ['DS', '3', 'IsocenterPosition'], + '0x012E': ['DS', '3', 'SurfaceEntryPoint'], + '0x0130': ['DS', '1', 'SourceToSurfaceDistance'], + '0x0131': ['FL', '1', 'AverageBeamDosePointSourceToExternalContourSurfaceDistance'], + '0x0132': ['FL', '1', 'SourceToExternalContourDistance'], + '0x0133': ['FL', '3', 'ExternalContourEntryPoint'], + '0x0134': ['DS', '1', 'CumulativeMetersetWeight'], + '0x0140': ['FL', '1', 'TableTopPitchAngle'], + '0x0142': ['CS', '1', 'TableTopPitchRotationDirection'], + '0x0144': ['FL', '1', 'TableTopRollAngle'], + '0x0146': ['CS', '1', 'TableTopRollRotationDirection'], + '0x0148': ['FL', '1', 'HeadFixationAngle'], + '0x014A': ['FL', '1', 'GantryPitchAngle'], + '0x014C': ['CS', '1', 'GantryPitchRotationDirection'], + '0x014E': ['FL', '1', 'GantryPitchAngleTolerance'], + '0x0180': ['SQ', '1', 'PatientSetupSequence'], + '0x0182': ['IS', '1', 'PatientSetupNumber'], + '0x0183': ['LO', '1', 'PatientSetupLabel'], + '0x0184': ['LO', '1', 'PatientAdditionalPosition'], + '0x0190': ['SQ', '1', 'FixationDeviceSequence'], + '0x0192': ['CS', '1', 'FixationDeviceType'], + '0x0194': ['SH', '1', 'FixationDeviceLabel'], + '0x0196': ['ST', '1', 'FixationDeviceDescription'], + '0x0198': ['SH', '1', 'FixationDevicePosition'], + '0x0199': ['FL', '1', 'FixationDevicePitchAngle'], + '0x019A': ['FL', '1', 'FixationDeviceRollAngle'], + '0x01A0': ['SQ', '1', 'ShieldingDeviceSequence'], + '0x01A2': ['CS', '1', 'ShieldingDeviceType'], + '0x01A4': ['SH', '1', 'ShieldingDeviceLabel'], + '0x01A6': ['ST', '1', 'ShieldingDeviceDescription'], + '0x01A8': ['SH', '1', 'ShieldingDevicePosition'], + '0x01B0': ['CS', '1', 'SetupTechnique'], + '0x01B2': ['ST', '1', 'SetupTechniqueDescription'], + '0x01B4': ['SQ', '1', 'SetupDeviceSequence'], + '0x01B6': ['CS', '1', 'SetupDeviceType'], + '0x01B8': ['SH', '1', 'SetupDeviceLabel'], + '0x01BA': ['ST', '1', 'SetupDeviceDescription'], + '0x01BC': ['DS', '1', 'SetupDeviceParameter'], + '0x01D0': ['ST', '1', 'SetupReferenceDescription'], + '0x01D2': ['DS', '1', 'TableTopVerticalSetupDisplacement'], + '0x01D4': ['DS', '1', 'TableTopLongitudinalSetupDisplacement'], + '0x01D6': ['DS', '1', 'TableTopLateralSetupDisplacement'], + '0x0200': ['CS', '1', 'BrachyTreatmentTechnique'], + '0x0202': ['CS', '1', 'BrachyTreatmentType'], + '0x0206': ['SQ', '1', 'TreatmentMachineSequence'], + '0x0210': ['SQ', '1', 'SourceSequence'], + '0x0212': ['IS', '1', 'SourceNumber'], + '0x0214': ['CS', '1', 'SourceType'], + '0x0216': ['LO', '1', 'SourceManufacturer'], + '0x0218': ['DS', '1', 'ActiveSourceDiameter'], + '0x021A': ['DS', '1', 'ActiveSourceLength'], + '0x021B': ['SH', '1', 'SourceModelID'], + '0x021C': ['LO', '1', 'SourceDescription'], + '0x0222': ['DS', '1', 'SourceEncapsulationNominalThickness'], + '0x0224': ['DS', '1', 'SourceEncapsulationNominalTransmission'], + '0x0226': ['LO', '1', 'SourceIsotopeName'], + '0x0228': ['DS', '1', 'SourceIsotopeHalfLife'], + '0x0229': ['CS', '1', 'SourceStrengthUnits'], + '0x022A': ['DS', '1', 'ReferenceAirKermaRate'], + '0x022B': ['DS', '1', 'SourceStrength'], + '0x022C': ['DA', '1', 'SourceStrengthReferenceDate'], + '0x022E': ['TM', '1', 'SourceStrengthReferenceTime'], + '0x0230': ['SQ', '1', 'ApplicationSetupSequence'], + '0x0232': ['CS', '1', 'ApplicationSetupType'], + '0x0234': ['IS', '1', 'ApplicationSetupNumber'], + '0x0236': ['LO', '1', 'ApplicationSetupName'], + '0x0238': ['LO', '1', 'ApplicationSetupManufacturer'], + '0x0240': ['IS', '1', 'TemplateNumber'], + '0x0242': ['SH', '1', 'TemplateType'], + '0x0244': ['LO', '1', 'TemplateName'], + '0x0250': ['DS', '1', 'TotalReferenceAirKerma'], + '0x0260': ['SQ', '1', 'BrachyAccessoryDeviceSequence'], + '0x0262': ['IS', '1', 'BrachyAccessoryDeviceNumber'], + '0x0263': ['SH', '1', 'BrachyAccessoryDeviceID'], + '0x0264': ['CS', '1', 'BrachyAccessoryDeviceType'], + '0x0266': ['LO', '1', 'BrachyAccessoryDeviceName'], + '0x026A': ['DS', '1', 'BrachyAccessoryDeviceNominalThickness'], + '0x026C': ['DS', '1', 'BrachyAccessoryDeviceNominalTransmission'], + '0x0280': ['SQ', '1', 'ChannelSequence'], + '0x0282': ['IS', '1', 'ChannelNumber'], + '0x0284': ['DS', '1', 'ChannelLength'], + '0x0286': ['DS', '1', 'ChannelTotalTime'], + '0x0288': ['CS', '1', 'SourceMovementType'], + '0x028A': ['IS', '1', 'NumberOfPulses'], + '0x028C': ['DS', '1', 'PulseRepetitionInterval'], + '0x0290': ['IS', '1', 'SourceApplicatorNumber'], + '0x0291': ['SH', '1', 'SourceApplicatorID'], + '0x0292': ['CS', '1', 'SourceApplicatorType'], + '0x0294': ['LO', '1', 'SourceApplicatorName'], + '0x0296': ['DS', '1', 'SourceApplicatorLength'], + '0x0298': ['LO', '1', 'SourceApplicatorManufacturer'], + '0x029C': ['DS', '1', 'SourceApplicatorWallNominalThickness'], + '0x029E': ['DS', '1', 'SourceApplicatorWallNominalTransmission'], + '0x02A0': ['DS', '1', 'SourceApplicatorStepSize'], + '0x02A2': ['IS', '1', 'TransferTubeNumber'], + '0x02A4': ['DS', '1', 'TransferTubeLength'], + '0x02B0': ['SQ', '1', 'ChannelShieldSequence'], + '0x02B2': ['IS', '1', 'ChannelShieldNumber'], + '0x02B3': ['SH', '1', 'ChannelShieldID'], + '0x02B4': ['LO', '1', 'ChannelShieldName'], + '0x02B8': ['DS', '1', 'ChannelShieldNominalThickness'], + '0x02BA': ['DS', '1', 'ChannelShieldNominalTransmission'], + '0x02C8': ['DS', '1', 'FinalCumulativeTimeWeight'], + '0x02D0': ['SQ', '1', 'BrachyControlPointSequence'], + '0x02D2': ['DS', '1', 'ControlPointRelativePosition'], + '0x02D4': ['DS', '3', 'ControlPoint3DPosition'], + '0x02D6': ['DS', '1', 'CumulativeTimeWeight'], + '0x02E0': ['CS', '1', 'CompensatorDivergence'], + '0x02E1': ['CS', '1', 'CompensatorMountingPosition'], + '0x02E2': ['DS', '1-n', 'SourceToCompensatorDistance'], + '0x02E3': ['FL', '1', 'TotalCompensatorTrayWaterEquivalentThickness'], + '0x02E4': ['FL', '1', 'IsocenterToCompensatorTrayDistance'], + '0x02E5': ['FL', '1', 'CompensatorColumnOffset'], + '0x02E6': ['FL', '1-n', 'IsocenterToCompensatorDistances'], + '0x02E7': ['FL', '1', 'CompensatorRelativeStoppingPowerRatio'], + '0x02E8': ['FL', '1', 'CompensatorMillingToolDiameter'], + '0x02EA': ['SQ', '1', 'IonRangeCompensatorSequence'], + '0x02EB': ['LT', '1', 'CompensatorDescription'], + '0x0302': ['IS', '1', 'RadiationMassNumber'], + '0x0304': ['IS', '1', 'RadiationAtomicNumber'], + '0x0306': ['SS', '1', 'RadiationChargeState'], + '0x0308': ['CS', '1', 'ScanMode'], + '0x030A': ['FL', '2', 'VirtualSourceAxisDistances'], + '0x030C': ['SQ', '1', 'SnoutSequence'], + '0x030D': ['FL', '1', 'SnoutPosition'], + '0x030F': ['SH', '1', 'SnoutID'], + '0x0312': ['IS', '1', 'NumberOfRangeShifters'], + '0x0314': ['SQ', '1', 'RangeShifterSequence'], + '0x0316': ['IS', '1', 'RangeShifterNumber'], + '0x0318': ['SH', '1', 'RangeShifterID'], + '0x0320': ['CS', '1', 'RangeShifterType'], + '0x0322': ['LO', '1', 'RangeShifterDescription'], + '0x0330': ['IS', '1', 'NumberOfLateralSpreadingDevices'], + '0x0332': ['SQ', '1', 'LateralSpreadingDeviceSequence'], + '0x0334': ['IS', '1', 'LateralSpreadingDeviceNumber'], + '0x0336': ['SH', '1', 'LateralSpreadingDeviceID'], + '0x0338': ['CS', '1', 'LateralSpreadingDeviceType'], + '0x033A': ['LO', '1', 'LateralSpreadingDeviceDescription'], + '0x033C': ['FL', '1', 'LateralSpreadingDeviceWaterEquivalentThickness'], + '0x0340': ['IS', '1', 'NumberOfRangeModulators'], + '0x0342': ['SQ', '1', 'RangeModulatorSequence'], + '0x0344': ['IS', '1', 'RangeModulatorNumber'], + '0x0346': ['SH', '1', 'RangeModulatorID'], + '0x0348': ['CS', '1', 'RangeModulatorType'], + '0x034A': ['LO', '1', 'RangeModulatorDescription'], + '0x034C': ['SH', '1', 'BeamCurrentModulationID'], + '0x0350': ['CS', '1', 'PatientSupportType'], + '0x0352': ['SH', '1', 'PatientSupportID'], + '0x0354': ['LO', '1', 'PatientSupportAccessoryCode'], + '0x0356': ['FL', '1', 'FixationLightAzimuthalAngle'], + '0x0358': ['FL', '1', 'FixationLightPolarAngle'], + '0x035A': ['FL', '1', 'MetersetRate'], + '0x0360': ['SQ', '1', 'RangeShifterSettingsSequence'], + '0x0362': ['LO', '1', 'RangeShifterSetting'], + '0x0364': ['FL', '1', 'IsocenterToRangeShifterDistance'], + '0x0366': ['FL', '1', 'RangeShifterWaterEquivalentThickness'], + '0x0370': ['SQ', '1', 'LateralSpreadingDeviceSettingsSequence'], + '0x0372': ['LO', '1', 'LateralSpreadingDeviceSetting'], + '0x0374': ['FL', '1', 'IsocenterToLateralSpreadingDeviceDistance'], + '0x0380': ['SQ', '1', 'RangeModulatorSettingsSequence'], + '0x0382': ['FL', '1', 'RangeModulatorGatingStartValue'], + '0x0384': ['FL', '1', 'RangeModulatorGatingStopValue'], + '0x0386': ['FL', '1', 'RangeModulatorGatingStartWaterEquivalentThickness'], + '0x0388': ['FL', '1', 'RangeModulatorGatingStopWaterEquivalentThickness'], + '0x038A': ['FL', '1', 'IsocenterToRangeModulatorDistance'], + '0x0390': ['SH', '1', 'ScanSpotTuneID'], + '0x0392': ['IS', '1', 'NumberOfScanSpotPositions'], + '0x0394': ['FL', '1-n', 'ScanSpotPositionMap'], + '0x0396': ['FL', '1-n', 'ScanSpotMetersetWeights'], + '0x0398': ['FL', '2', 'ScanningSpotSize'], + '0x039A': ['IS', '1', 'NumberOfPaintings'], + '0x03A0': ['SQ', '1', 'IonToleranceTableSequence'], + '0x03A2': ['SQ', '1', 'IonBeamSequence'], + '0x03A4': ['SQ', '1', 'IonBeamLimitingDeviceSequence'], + '0x03A6': ['SQ', '1', 'IonBlockSequence'], + '0x03A8': ['SQ', '1', 'IonControlPointSequence'], + '0x03AA': ['SQ', '1', 'IonWedgeSequence'], + '0x03AC': ['SQ', '1', 'IonWedgePositionSequence'], + '0x0401': ['SQ', '1', 'ReferencedSetupImageSequence'], + '0x0402': ['ST', '1', 'SetupImageComment'], + '0x0410': ['SQ', '1', 'MotionSynchronizationSequence'], + '0x0412': ['FL', '3', 'ControlPointOrientation'], + '0x0420': ['SQ', '1', 'GeneralAccessorySequence'], + '0x0421': ['SH', '1', 'GeneralAccessoryID'], + '0x0422': ['ST', '1', 'GeneralAccessoryDescription'], + '0x0423': ['CS', '1', 'GeneralAccessoryType'], + '0x0424': ['IS', '1', 'GeneralAccessoryNumber'], + '0x0425': ['FL', '1', 'SourceToGeneralAccessoryDistance'], + '0x0431': ['SQ', '1', 'ApplicatorGeometrySequence'], + '0x0432': ['CS', '1', 'ApplicatorApertureShape'], + '0x0433': ['FL', '1', 'ApplicatorOpening'], + '0x0434': ['FL', '1', 'ApplicatorOpeningX'], + '0x0435': ['FL', '1', 'ApplicatorOpeningY'], + '0x0436': ['FL', '1', 'SourceToApplicatorMountingPositionDistance'], + '0x0440': ['IS', '1', 'NumberOfBlockSlabItems'], + '0x0441': ['SQ', '1', 'BlockSlabSequence'], + '0x0442': ['DS', '1', 'BlockSlabThickness'], + '0x0443': ['US', '1', 'BlockSlabNumber'], + '0x0450': ['SQ', '1', 'DeviceMotionControlSequence'], + '0x0451': ['CS', '1', 'DeviceMotionExecutionMode'], + '0x0452': ['CS', '1', 'DeviceMotionObservationMode'], + '0x0453': ['SQ', '1', 'DeviceMotionParameterCodeSequence'] + }, + '0x300C': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['SQ', '1', 'ReferencedRTPlanSequence'], + '0x0004': ['SQ', '1', 'ReferencedBeamSequence'], + '0x0006': ['IS', '1', 'ReferencedBeamNumber'], + '0x0007': ['IS', '1', 'ReferencedReferenceImageNumber'], + '0x0008': ['DS', '1', 'StartCumulativeMetersetWeight'], + '0x0009': ['DS', '1', 'EndCumulativeMetersetWeight'], + '0x000A': ['SQ', '1', 'ReferencedBrachyApplicationSetupSequence'], + '0x000C': ['IS', '1', 'ReferencedBrachyApplicationSetupNumber'], + '0x000E': ['IS', '1', 'ReferencedSourceNumber'], + '0x0020': ['SQ', '1', 'ReferencedFractionGroupSequence'], + '0x0022': ['IS', '1', 'ReferencedFractionGroupNumber'], + '0x0040': ['SQ', '1', 'ReferencedVerificationImageSequence'], + '0x0042': ['SQ', '1', 'ReferencedReferenceImageSequence'], + '0x0050': ['SQ', '1', 'ReferencedDoseReferenceSequence'], + '0x0051': ['IS', '1', 'ReferencedDoseReferenceNumber'], + '0x0055': ['SQ', '1', 'BrachyReferencedDoseReferenceSequence'], + '0x0060': ['SQ', '1', 'ReferencedStructureSetSequence'], + '0x006A': ['IS', '1', 'ReferencedPatientSetupNumber'], + '0x0080': ['SQ', '1', 'ReferencedDoseSequence'], + '0x00A0': ['IS', '1', 'ReferencedToleranceTableNumber'], + '0x00B0': ['SQ', '1', 'ReferencedBolusSequence'], + '0x00C0': ['IS', '1', 'ReferencedWedgeNumber'], + '0x00D0': ['IS', '1', 'ReferencedCompensatorNumber'], + '0x00E0': ['IS', '1', 'ReferencedBlockNumber'], + '0x00F0': ['IS', '1', 'ReferencedControlPointIndex'], + '0x00F2': ['SQ', '1', 'ReferencedControlPointSequence'], + '0x00F4': ['IS', '1', 'ReferencedStartControlPointIndex'], + '0x00F6': ['IS', '1', 'ReferencedStopControlPointIndex'], + '0x0100': ['IS', '1', 'ReferencedRangeShifterNumber'], + '0x0102': ['IS', '1', 'ReferencedLateralSpreadingDeviceNumber'], + '0x0104': ['IS', '1', 'ReferencedRangeModulatorNumber'], + '0x0111': ['SQ', '1', 'OmittedBeamTaskSequence'], + '0x0112': ['CS', '1', 'ReasonForOmission'], + '0x0113': ['LO', '1', 'ReasonForOmissionDescription'] + }, + '0x300E': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0002': ['CS', '1', 'ApprovalStatus'], + '0x0004': ['DA', '1', 'ReviewDate'], + '0x0005': ['TM', '1', 'ReviewTime'], + '0x0008': ['PN', '1', 'ReviewerName'] + }, + '0x4000': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['LT', '1', 'Arbitrary'], + '0x4000': ['LT', '1', 'TextComments'] + }, + '0x4008': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0040': ['SH', '1', 'ResultsID'], + '0x0042': ['LO', '1', 'ResultsIDIssuer'], + '0x0050': ['SQ', '1', 'ReferencedInterpretationSequence'], + '0x00FF': ['CS', '1', 'ReportProductionStatusTrial'], + '0x0100': ['DA', '1', 'InterpretationRecordedDate'], + '0x0101': ['TM', '1', 'InterpretationRecordedTime'], + '0x0102': ['PN', '1', 'InterpretationRecorder'], + '0x0103': ['LO', '1', 'ReferenceToRecordedSound'], + '0x0108': ['DA', '1', 'InterpretationTranscriptionDate'], + '0x0109': ['TM', '1', 'InterpretationTranscriptionTime'], + '0x010A': ['PN', '1', 'InterpretationTranscriber'], + '0x010B': ['ST', '1', 'InterpretationText'], + '0x010C': ['PN', '1', 'InterpretationAuthor'], + '0x0111': ['SQ', '1', 'InterpretationApproverSequence'], + '0x0112': ['DA', '1', 'InterpretationApprovalDate'], + '0x0113': ['TM', '1', 'InterpretationApprovalTime'], + '0x0114': ['PN', '1', 'PhysicianApprovingInterpretation'], + '0x0115': ['LT', '1', 'InterpretationDiagnosisDescription'], + '0x0117': ['SQ', '1', 'InterpretationDiagnosisCodeSequence'], + '0x0118': ['SQ', '1', 'ResultsDistributionListSequence'], + '0x0119': ['PN', '1', 'DistributionName'], + '0x011A': ['LO', '1', 'DistributionAddress'], + '0x0200': ['SH', '1', 'InterpretationID'], + '0x0202': ['LO', '1', 'InterpretationIDIssuer'], + '0x0210': ['CS', '1', 'InterpretationTypeID'], + '0x0212': ['CS', '1', 'InterpretationStatusID'], + '0x0300': ['ST', '1', 'Impressions'], + '0x4000': ['ST', '1', 'ResultsComments'] + }, + '0x4010': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['CS', '1', 'LowEnergyDetectors'], + '0x0002': ['CS', '1', 'HighEnergyDetectors'], + '0x0004': ['SQ', '1', 'DetectorGeometrySequence'], + '0x1001': ['SQ', '1', 'ThreatROIVoxelSequence'], + '0x1004': ['FL', '3', 'ThreatROIBase'], + '0x1005': ['FL', '3', 'ThreatROIExtents'], + '0x1006': ['OB', '1', 'ThreatROIBitmap'], + '0x1007': ['SH', '1', 'RouteSegmentID'], + '0x1008': ['CS', '1', 'GantryType'], + '0x1009': ['CS', '1', 'OOIOwnerType'], + '0x100A': ['SQ', '1', 'RouteSegmentSequence'], + '0x1010': ['US', '1', 'PotentialThreatObjectID'], + '0x1011': ['SQ', '1', 'ThreatSequence'], + '0x1012': ['CS', '1', 'ThreatCategory'], + '0x1013': ['LT', '1', 'ThreatCategoryDescription'], + '0x1014': ['CS', '1', 'ATDAbilityAssessment'], + '0x1015': ['CS', '1', 'ATDAssessmentFlag'], + '0x1016': ['FL', '1', 'ATDAssessmentProbability'], + '0x1017': ['FL', '1', 'Mass'], + '0x1018': ['FL', '1', 'Density'], + '0x1019': ['FL', '1', 'ZEffective'], + '0x101A': ['SH', '1', 'BoardingPassID'], + '0x101B': ['FL', '3', 'CenterOfMass'], + '0x101C': ['FL', '3', 'CenterOfPTO'], + '0x101D': ['FL', '6-n', 'BoundingPolygon'], + '0x101E': ['SH', '1', 'RouteSegmentStartLocationID'], + '0x101F': ['SH', '1', 'RouteSegmentEndLocationID'], + '0x1020': ['CS', '1', 'RouteSegmentLocationIDType'], + '0x1021': ['CS', '1-n', 'AbortReason'], + '0x1023': ['FL', '1', 'VolumeOfPTO'], + '0x1024': ['CS', '1', 'AbortFlag'], + '0x1025': ['DT', '1', 'RouteSegmentStartTime'], + '0x1026': ['DT', '1', 'RouteSegmentEndTime'], + '0x1027': ['CS', '1', 'TDRType'], + '0x1028': ['CS', '1', 'InternationalRouteSegment'], + '0x1029': ['LO', '1-n', 'ThreatDetectionAlgorithmandVersion'], + '0x102A': ['SH', '1', 'AssignedLocation'], + '0x102B': ['DT', '1', 'AlarmDecisionTime'], + '0x1031': ['CS', '1', 'AlarmDecision'], + '0x1033': ['US', '1', 'NumberOfTotalObjects'], + '0x1034': ['US', '1', 'NumberOfAlarmObjects'], + '0x1037': ['SQ', '1', 'PTORepresentationSequence'], + '0x1038': ['SQ', '1', 'ATDAssessmentSequence'], + '0x1039': ['CS', '1', 'TIPType'], + '0x103A': ['CS', '1', 'DICOSVersion'], + '0x1041': ['DT', '1', 'OOIOwnerCreationTime'], + '0x1042': ['CS', '1', 'OOIType'], + '0x1043': ['FL', '3', 'OOISize'], + '0x1044': ['CS', '1', 'AcquisitionStatus'], + '0x1045': ['SQ', '1', 'BasisMaterialsCodeSequence'], + '0x1046': ['CS', '1', 'PhantomType'], + '0x1047': ['SQ', '1', 'OOIOwnerSequence'], + '0x1048': ['CS', '1', 'ScanType'], + '0x1051': ['LO', '1', 'ItineraryID'], + '0x1052': ['SH', '1', 'ItineraryIDType'], + '0x1053': ['LO', '1', 'ItineraryIDAssigningAuthority'], + '0x1054': ['SH', '1', 'RouteID'], + '0x1055': ['SH', '1', 'RouteIDAssigningAuthority'], + '0x1056': ['CS', '1', 'InboundArrivalType'], + '0x1058': ['SH', '1', 'CarrierID'], + '0x1059': ['CS', '1', 'CarrierIDAssigningAuthority'], + '0x1060': ['FL', '3', 'SourceOrientation'], + '0x1061': ['FL', '3', 'SourcePosition'], + '0x1062': ['FL', '1', 'BeltHeight'], + '0x1064': ['SQ', '1', 'AlgorithmRoutingCodeSequence'], + '0x1067': ['CS', '1', 'TransportClassification'], + '0x1068': ['LT', '1', 'OOITypeDescriptor'], + '0x1069': ['FL', '1', 'TotalProcessingTime'], + '0x106C': ['OB', '1', 'DetectorCalibrationData'], + '0x106D': ['CS', '1', 'AdditionalScreeningPerformed'], + '0x106E': ['CS', '1', 'AdditionalInspectionSelectionCriteria'], + '0x106F': ['SQ', '1', 'AdditionalInspectionMethodSequence'], + '0x1070': ['CS', '1', 'AITDeviceType'], + '0x1071': ['SQ', '1', 'QRMeasurementsSequence'], + '0x1072': ['SQ', '1', 'TargetMaterialSequence'], + '0x1073': ['FD', '1', 'SNRThreshold'], + '0x1075': ['DS', '1', 'ImageScaleRepresentation'], + '0x1076': ['SQ', '1', 'ReferencedPTOSequence'], + '0x1077': ['SQ', '1', 'ReferencedTDRInstanceSequence'], + '0x1078': ['ST', '1', 'PTOLocationDescription'], + '0x1079': ['SQ', '1', 'AnomalyLocatorIndicatorSequence'], + '0x107A': ['FL', '3', 'AnomalyLocatorIndicator'], + '0x107B': ['SQ', '1', 'PTORegionSequence'], + '0x107C': ['CS', '1', 'InspectionSelectionCriteria'], + '0x107D': ['SQ', '1', 'SecondaryInspectionMethodSequence'], + '0x107E': ['DS', '6', 'PRCSToRCSOrientation'] + }, + '0x4FFE': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0001': ['SQ', '1', 'MACParametersSequence'] + }, + '0x5000': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0005': ['US', '1', 'CurveDimensions'], + '0x0010': ['US', '1', 'NumberOfPoints'], + '0x0020': ['CS', '1', 'TypeOfData'], + '0x0022': ['LO', '1', 'CurveDescription'], + '0x0030': ['SH', '1-n', 'AxisUnits'], + '0x0040': ['SH', '1-n', 'AxisLabels'], + '0x0103': ['US', '1', 'DataValueRepresentation'], + '0x0104': ['US', '1-n', 'MinimumCoordinateValue'], + '0x0105': ['US', '1-n', 'MaximumCoordinateValue'], + '0x0106': ['SH', '1-n', 'CurveRange'], + '0x0110': ['US', '1-n', 'CurveDataDescriptor'], + '0x0112': ['US', '1-n', 'CoordinateStartValue'], + '0x0114': ['US', '1-n', 'CoordinateStepValue'], + '0x1001': ['CS', '1', 'CurveActivationLayer'], + '0x2000': ['US', '1', 'AudioType'], + '0x2002': ['US', '1', 'AudioSampleFormat'], + '0x2004': ['US', '1', 'NumberOfChannels'], + '0x2006': ['UL', '1', 'NumberOfSamples'], + '0x2008': ['UL', '1', 'SampleRate'], + '0x200A': ['UL', '1', 'TotalTime'], + '0x200C': ['ox', '1', 'AudioSampleData'], + '0x200E': ['LT', '1', 'AudioComments'], + '0x2500': ['LO', '1', 'CurveLabel'], + '0x2600': ['SQ', '1', 'CurveReferencedOverlaySequence'], + '0x2610': ['US', '1', 'CurveReferencedOverlayGroup'], + '0x3000': ['ox', '1', 'CurveData'] + }, + '0x5200': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x9229': ['SQ', '1', 'SharedFunctionalGroupsSequence'], + '0x9230': ['SQ', '1', 'PerFrameFunctionalGroupsSequence'] + }, + '0x5400': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0100': ['SQ', '1', 'WaveformSequence'], + '0x0110': ['ox', '1', 'ChannelMinimumValue'], + '0x0112': ['ox', '1', 'ChannelMaximumValue'], + '0x1004': ['US', '1', 'WaveformBitsAllocated'], + '0x1006': ['CS', '1', 'WaveformSampleInterpretation'], + '0x100A': ['ox', '1', 'WaveformPaddingValue'], + '0x1010': ['ox', '1', 'WaveformData'] + }, + '0x5600': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['OF', '1', 'FirstOrderPhaseCorrectionAngle'], + '0x0020': ['OF', '1', 'SpectroscopyData'] + }, + '0x6000': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['US', '1', 'OverlayRows'], + '0x0011': ['US', '1', 'OverlayColumns'], + '0x0012': ['US', '1', 'OverlayPlanes'], + '0x0015': ['IS', '1', 'NumberOfFramesInOverlay'], + '0x0022': ['LO', '1', 'OverlayDescription'], + '0x0040': ['CS', '1', 'OverlayType'], + '0x0045': ['LO', '1', 'OverlaySubtype'], + '0x0050': ['SS', '2', 'OverlayOrigin'], + '0x0051': ['US', '1', 'ImageFrameOrigin'], + '0x0052': ['US', '1', 'OverlayPlaneOrigin'], + '0x0060': ['CS', '1', 'OverlayCompressionCode'], + '0x0061': ['SH', '1', 'OverlayCompressionOriginator'], + '0x0062': ['SH', '1', 'OverlayCompressionLabel'], + '0x0063': ['CS', '1', 'OverlayCompressionDescription'], + '0x0066': ['AT', '1-n', 'OverlayCompressionStepPointers'], + '0x0068': ['US', '1', 'OverlayRepeatInterval'], + '0x0069': ['US', '1', 'OverlayBitsGrouped'], + '0x0100': ['US', '1', 'OverlayBitsAllocated'], + '0x0102': ['US', '1', 'OverlayBitPosition'], + '0x0110': ['CS', '1', 'OverlayFormat'], + '0x0200': ['US', '1', 'OverlayLocation'], + '0x0800': ['CS', '1-n', 'OverlayCodeLabel'], + '0x0802': ['US', '1', 'OverlayNumberOfTables'], + '0x0803': ['AT', '1-n', 'OverlayCodeTableLocation'], + '0x0804': ['US', '1', 'OverlayBitsForCodeWord'], + '0x1001': ['CS', '1', 'OverlayActivationLayer'], + '0x1100': ['US', '1', 'OverlayDescriptorGray'], + '0x1101': ['US', '1', 'OverlayDescriptorRed'], + '0x1102': ['US', '1', 'OverlayDescriptorGreen'], + '0x1103': ['US', '1', 'OverlayDescriptorBlue'], + '0x1200': ['US', '1-n', 'OverlaysGray'], + '0x1201': ['US', '1-n', 'OverlaysRed'], + '0x1202': ['US', '1-n', 'OverlaysGreen'], + '0x1203': ['US', '1-n', 'OverlaysBlue'], + '0x1301': ['IS', '1', 'ROIArea'], + '0x1302': ['DS', '1', 'ROIMean'], + '0x1303': ['DS', '1', 'ROIStandardDeviation'], + '0x1500': ['LO', '1', 'OverlayLabel'], + '0x3000': ['ox', '1', 'OverlayData'], + '0x4000': ['LT', '1', 'OverlayComments'] + }, + '0x7FE0': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0008': ['OF', '1', 'FloatPixelData'], + '0x0009': ['OD', '1', 'DoubleFloatPixelData'], + '0x0010': ['ox', '1', 'PixelData'], + '0x0020': ['OW', '1', 'CoefficientsSDVN'], + '0x0030': ['OW', '1', 'CoefficientsSDHN'], + '0x0040': ['OW', '1', 'CoefficientsSDDN'] + }, + '0x7F00': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0x0010': ['ox', '1', 'VariablePixelData'], + '0x0011': ['US', '1', 'VariableNextDataGroup'], + '0x0020': ['OW', '1', 'VariableCoefficientsSDVN'], + '0x0030': ['OW', '1', 'VariableCoefficientsSDHN'], + '0x0040': ['OW', '1', 'VariableCoefficientsSDDN'] + }, + '0xFFFA': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0xFFFA': ['SQ', '1', 'DigitalSignaturesSequence'] + }, + '0xFFFC': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0xFFFC': ['OB', '1', 'DataSetTrailingPadding'] + }, + '0xFFFE': { + '0x0000': ['UL', '1', 'GenericGroupLength'], + '0xE000': ['NONE', '1', 'Item'], + '0xE00D': ['NONE', '1', 'ItemDelimitationItem'], + '0xE0DD': ['NONE', '1', 'SequenceDelimitationItem'] + } +}; // dwv.dicom.Dictionnary + +// taken from gdcm-2.6.1\Source\DataDictionary\GroupName.dic +// -> removed duplicates (commented) +dwv.dicom.TagGroups = { + 'x0000' :'Command', + 'x0002': 'Meta Element', + 'x0004': 'File Set', + //'x0004': 'Directory', + 'x0008': 'Identifying', + 'x0009': 'SPI Identifying', + 'x0010': 'Patient', + 'x0012': 'Clinical Trial', + 'x0018': 'Acquisition', + 'x0019': 'SPI Acquisition', + 'x0020': 'Image', + 'x0021': 'SPI Image', + 'x0022': 'Ophtalmology', + 'x0028': 'Image Presentation', + 'x0032': 'Study', + 'x0038': 'Visit', + 'x003A': 'Waveform', + 'x0040': 'Procedure', + //'x0040': ''Modality Worklist', + 'x0042': 'Encapsulated Document', + 'x0050': 'Device Informations', + //'x0050': 'XRay Angio Device', + 'x0054': 'Nuclear Medicine', + 'x0060': 'Histogram', + 'x0070': 'Presentation State', + 'x0072': 'Hanging Protocol', + 'x0088': 'Storage', + //'x0088': 'Medicine', + 'x0100': 'Authorization', + 'x0400': 'Digital Signature', + 'x1000': 'Code Table', + 'x1010': 'Zonal Map', + 'x2000': 'Film Session', + 'x2010': 'Film Box', + 'x2020': 'Image Box', + 'x2030': 'Annotation', + 'x2040': 'Overlay Box', + 'x2050': 'Presentation LUT', + 'x2100': 'Print Job', + 'x2110': 'Printer', + 'x2120': 'Queue', + 'x2130': 'Print Content', + 'x2200': 'Media Creation', + 'x3002': 'RT Image', + 'x3004': 'RT Dose', + 'x3006': 'RT StructureSet', + 'x3008': 'RT Treatment', + 'x300A': 'RT Plan', + 'x300C': 'RT Relationship', + 'x300E': 'RT Approval', + 'x4000': 'Text', + 'x4008': 'Results', + 'x4FFE': 'MAC Parameters', + 'x5000': 'Curve', + 'x5002': 'Curve', + 'x5004': 'Curve', + 'x5006': 'Curve', + 'x5008': 'Curve', + 'x500A': 'Curve', + 'x500C': 'Curve', + 'x500E': 'Curve', + 'x5400': 'Waveform Data', + 'x6000': 'Overlays', + 'x6002': 'Overlays', + 'x6004': 'Overlays', + 'x6008': 'Overlays', + 'x600A': 'Overlays', + 'x600C': 'Overlays', + 'x600E': 'Overlays', + 'xFFFC': 'Generic', + 'x7FE0': 'Pixel Data', + 'xFFFF': 'Unknown' + }; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.gui = dwv.gui || {}; +/** @namespace */ +dwv.gui.base = dwv.gui.base || {}; +/** @namespace */ +dwv.gui.filter = dwv.gui.filter || {}; +/** @namespace */ +dwv.gui.filter.base = dwv.gui.filter.base || {}; + +/** + * Filter tool base gui. + * @constructor + */ +dwv.gui.base.Filter = function (app) +{ + /** + * Setup the filter tool HTML. + */ + this.setup = function (list) + { + // filter select + var filterSelector = dwv.html.createHtmlSelect("filterSelect", list, "filter"); + filterSelector.onchange = app.onChangeFilter; + + // filter list element + var filterLi = dwv.html.createHiddenElement("li", "filterLi"); + filterLi.className += " ui-block-b"; + filterLi.appendChild(filterSelector); + + // append element + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + dwv.html.appendElement(node, filterLi); + }; + + /** + * Display the tool HTML. + * @param {Boolean} flag True to display, false to hide. + */ + this.display = function (flag) + { + var node = app.getElement("filterLi"); + dwv.html.displayElement(node, flag); + }; + + /** + * Initialise the tool HTML. + */ + this.initialise = function () + { + // filter select: reset selected options + var filterSelector = app.getElement("filterSelect"); + filterSelector.selectedIndex = 0; + // refresh + dwv.gui.refreshElement(filterSelector); + }; + +}; // class dwv.gui.base.Filter + +/** + * Threshold filter base gui. + * @constructor + */ +dwv.gui.base.Threshold = function (app) +{ + /** + * Threshold slider. + * @private + * @type Object + */ + var slider = new dwv.gui.Slider(app); + + /** + * Setup the threshold filter HTML. + */ + this.setup = function () + { + // threshold list element + var thresholdLi = dwv.html.createHiddenElement("li", "thresholdLi"); + thresholdLi.className += " ui-block-c"; + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // append threshold + node.appendChild(thresholdLi); + // threshold slider + slider.append(); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Clear the threshold filter HTML. + * @param {Boolean} flag True to display, false to hide. + */ + this.display = function (flag) + { + // only initialise at display time + // (avoids min/max calculation at startup) + if (flag) { + slider.initialise(); + } + + var node = app.getElement("thresholdLi"); + dwv.html.displayElement(node, flag); + }; + + /** + * Initialise the threshold filter HTML. + */ + this.initialise = function () + { + // nothing to do + }; + +}; // class dwv.gui.base.Threshold + +/** + * Create the apply filter button. + */ +dwv.gui.filter.base.createFilterApplyButton = function (app) +{ + var button = document.createElement("button"); + button.id = "runFilterButton"; + button.onclick = app.onRunFilter; + button.setAttribute("style","width:100%; margin-top:0.5em;"); + button.setAttribute("class","ui-btn ui-btn-b"); + button.appendChild(document.createTextNode(dwv.i18n("basics.apply"))); + return button; +}; + +/** + * Sharpen filter base gui. + * @constructor + */ +dwv.gui.base.Sharpen = function (app) +{ + /** + * Setup the sharpen filter HTML. + */ + this.setup = function () + { + // sharpen list element + var sharpenLi = dwv.html.createHiddenElement("li", "sharpenLi"); + sharpenLi.className += " ui-block-c"; + sharpenLi.appendChild( dwv.gui.filter.base.createFilterApplyButton(app) ); + // append element + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + dwv.html.appendElement(node, sharpenLi); + }; + + /** + * Display the sharpen filter HTML. + * @param {Boolean} flag True to display, false to hide. + */ + this.display = function (flag) + { + var node = app.getElement("sharpenLi"); + dwv.html.displayElement(node, flag); + }; + +}; // class dwv.gui.base.Sharpen + +/** + * Sobel filter base gui. + * @constructor + */ +dwv.gui.base.Sobel = function (app) +{ + /** + * Setup the sobel filter HTML. + */ + this.setup = function () + { + // sobel list element + var sobelLi = dwv.html.createHiddenElement("li", "sobelLi"); + sobelLi.className += " ui-block-c"; + sobelLi.appendChild( dwv.gui.filter.base.createFilterApplyButton(app) ); + // append element + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + dwv.html.appendElement(node, sobelLi); + }; + + /** + * Display the sobel filter HTML. + * @param {Boolean} flag True to display, false to hide. + */ + this.display = function (flag) + { + var node = app.getElement("sobelLi"); + dwv.html.displayElement(node, flag); + }; + +}; // class dwv.gui.base.Sobel + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; + +/** + * Get the size of the image display window. + */ +dwv.gui.base.getWindowSize = function () +{ + return { 'width': window.innerWidth, 'height': window.innerHeight - 147 }; +}; + +/** + * Ask some text to the user. + * @param {String} message Text to display to the user. + * @param {String} defaultText Default value displayed in the text input field. + * @return {String} Text entered by the user. + */ +dwv.gui.base.prompt = function (message, defaultText) +{ + return prompt(message, defaultText); +}; + +/** + * Display a progress value. + * @param {Number} percent The progress percentage. + */ +dwv.gui.base.displayProgress = function (/*percent*/) +{ + // default does nothing... +}; + +/** + * Focus the view on the image. + */ +dwv.gui.base.focusImage = function () +{ + // default does nothing... +}; + +/** + * Post process a HTML table. + * @param {Object} table The HTML table to process. + * @return The processed HTML table. + */ +dwv.gui.base.postProcessTable = function (/*table*/) +{ + // default does nothing... +}; + +/** + * Get a HTML element associated to a container div. + * @param {Number} containerDivId The id of the container div. + * @param {String} name The name or id to find. + * @return {Object} The found element or null. + */ +dwv.gui.base.getElement = function (containerDivId, name) +{ + // get by class in the container div + var parent = document.getElementById(containerDivId); + if ( !parent ) { + return null; + } + var elements = parent.getElementsByClassName(name); + // getting the last element since some libraries (ie jquery-mobile) create + // span in front of regular tags (such as select)... + var element = elements[elements.length-1]; + // if not found get by id with 'containerDivId-className' + if ( typeof element === "undefined" ) { + element = document.getElementById(containerDivId + '-' + name); + } + return element; + }; + + /** + * Refresh a HTML element. Mainly for jquery-mobile. + * @param {String} element The HTML element to refresh. + */ +dwv.gui.base.refreshElement = function (/*element*/) +{ + // base does nothing... +}; + +/** + * Set the selected item of a HTML select. + * @param {String} element The HTML select element. + * @param {String} value The value of the option to mark as selected. + */ +dwv.gui.setSelected = function (element, value) +{ + if ( element ) { + var index = 0; + for( index in element.options){ + if( element.options[index].value === value ) { + break; + } + } + element.selectedIndex = index; + dwv.gui.refreshElement(element); + } +}; + +/** + * Slider base gui. + * @constructor + */ +dwv.gui.base.Slider = function (app) +{ + /** + * Append the slider HTML. + */ + this.append = function () + { + // default values + var min = 0; + var max = 1; + + // jquery-mobile range slider + // minimum input + var inputMin = document.createElement("input"); + inputMin.id = "threshold-min"; + inputMin.type = "range"; + inputMin.max = max; + inputMin.min = min; + inputMin.value = min; + // maximum input + var inputMax = document.createElement("input"); + inputMax.id = "threshold-max"; + inputMax.type = "range"; + inputMax.max = max; + inputMax.min = min; + inputMax.value = max; + // slicer div + var div = document.createElement("div"); + div.id = "threshold-div"; + div.setAttribute("data-role", "rangeslider"); + div.appendChild(inputMin); + div.appendChild(inputMax); + div.setAttribute("data-mini", "true"); + // append to document + app.getElement("thresholdLi").appendChild(div); + // bind change + $("#threshold-div").on("change", + function(/*event*/) { + app.onChangeMinMax( + { "min":$("#threshold-min").val(), + "max":$("#threshold-max").val() } ); + } + ); + // refresh + dwv.gui.refreshElement(app.getElement("toolList")); + }; + + /** + * Initialise the slider HTML. + */ + this.initialise = function () + { + var min = app.getImage().getDataRange().min; + var max = app.getImage().getDataRange().max; + + // minimum input + var inputMin = document.getElementById("threshold-min"); + inputMin.max = max; + inputMin.min = min; + inputMin.value = min; + // maximum input + var inputMax = document.getElementById("threshold-max"); + inputMax.max = max; + inputMax.min = min; + inputMax.value = max; + // refresh + dwv.gui.refreshElement(app.getElement("toolList")); + }; + +}; // class dwv.gui.base.Slider + +/** + * DICOM tags base gui. + * @param {Object} app The associated application. + * @constructor + */ +dwv.gui.base.DicomTags = function (app) +{ + /** + * Update the DICOM tags table with the input info. + * @param {Object} dataInfo The data information. + */ + this.update = function (dataInfo) + { + // HTML node + var node = app.getElement("tags"); + if( node === null ) { + console.warn("Cannot find a node to append the DICOM tags."); + return; + } + // remove possible previous + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + + // exit if no tags + if (dataInfo.length === 0) { + console.warn("No DICOM tags to show."); + return; + } + + // tags HTML table + var table = dwv.html.toTable(dataInfo); + table.className = "tagsTable"; + + // optional gui specific table post process + dwv.gui.postProcessTable(table); + + // check processed table + if (table.rows.length === 0) { + console.warn("The processed table does not contain data."); + return; + } + + // translate first row + dwv.html.translateTableRow(table.rows.item(0)); + + // append search form + node.appendChild(dwv.html.getHtmlSearchForm(table)); + // append tags table + node.appendChild(table); + + // refresh + dwv.gui.refreshElement(node); + }; + +}; // class dwv.gui.base.DicomTags + +/** + * Drawing list base gui. + * @param {Object} app The associated application. + * @constructor + */ +dwv.gui.base.DrawList = function (app) +{ + /** + * Closure to self. + */ + var self = this; + + /** + * Update the draw list html element + * @param {Object} event A change event, decides if the table is editable or not. + */ + this.update = function (event) + { + var isEditable = false; + if (typeof event.editable !== "undefined") { + isEditable = event.editable; + } + + // HTML node + var node = app.getElement("drawList"); + if( node === null ) { + console.warn("Cannot find a node to append the drawing list."); + return; + } + // remove possible previous + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + + // drawing details + var drawDisplayDetails = app.getDrawDisplayDetails(); + + // exit if no details + if (drawDisplayDetails.length === 0) { + return; + } + + // tags HTML table + var table = dwv.html.toTable(drawDisplayDetails); + table.className = "drawsTable"; + + // optional gui specific table post process + dwv.gui.postProcessTable(table); + + // check processed table + if (table.rows.length === 0) { + console.warn("The processed table does not contain data."); + return; + } + + // translate first row + dwv.html.translateTableRow(table.rows.item(0)); + + // translate shape names + dwv.html.translateTableColumn(table, 3, "shape", "name"); + + // create a color onkeyup handler + var createColorOnKeyUp = function (details) { + return function () { + details.color = this.value; + app.updateDraw(details); + }; + }; + // create a text onkeyup handler + var createTextOnKeyUp = function (details) { + return function () { + details.label = this.value; + app.updateDraw(details); + }; + }; + // create a long text onkeyup handler + var createLongTextOnKeyUp = function (details) { + return function () { + details.description = this.value; + app.updateDraw(details); + }; + }; + // create a row onclick handler + var createRowOnClick = function (slice, frame) { + return function () { + // update slice + var pos = app.getViewController().getCurrentPosition(); + pos.k = slice; + app.getViewController().setCurrentPosition(pos); + // update frame + app.getViewController().setCurrentFrame(frame); + // focus on the image + dwv.gui.focusImage(); + }; + }; + // create visibility handler + var createVisibleOnClick = function (details) { + return function () { + app.toogleGroupVisibility(details); + }; + }; + + // append visible column to the header row + var row0 = table.rows.item(0); + var cell00 = row0.insertCell(0); + cell00.outerHTML = "" + dwv.i18n("basics.visible") + ""; + + // loop through rows + for (var r = 1; r < table.rows.length; ++r) { + var drawId = r - 1; + var drawDetails = drawDisplayDetails[drawId]; + var row = table.rows.item(r); + var cells = row.cells; + + // loop through cells + for (var c = 0; c < cells.length; ++c) { + // show short ID + if (c === 0) { + cells[c].firstChild.data = cells[c].firstChild.data.substring(0, 5); + } + + if (isEditable) { + // color + if (c === 4) { + dwv.html.makeCellEditable(cells[c], createColorOnKeyUp(drawDetails), "color"); + } + // text + else if (c === 5) { + dwv.html.makeCellEditable(cells[c], createTextOnKeyUp(drawDetails)); + } + // long text + else if (c === 6) { + dwv.html.makeCellEditable(cells[c], createLongTextOnKeyUp(drawDetails)); + } + } + else { + // id: link to image + cells[0].onclick = createRowOnClick( + cells[1].firstChild.data, + cells[2].firstChild.data); + cells[0].onmouseover = dwv.html.setCursorToPointer; + cells[0].onmouseout = dwv.html.setCursorToDefault; + // color: just display the input color with no callback + if (c === 4) { + dwv.html.makeCellEditable(cells[c], null, "color"); + } + } + } + + // append visible column + var cell0 = row.insertCell(0); + var input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.checked = app.isGroupVisible(drawDetails); + input.onclick = createVisibleOnClick(drawDetails); + cell0.appendChild(input); + } + + // editable checkbox + var tickBox = document.createElement("input"); + tickBox.setAttribute("type", "checkbox"); + tickBox.id = "checkbox-editable"; + tickBox.checked = isEditable; + tickBox.onclick = function () { self.update({"editable": this.checked}); }; + // checkbox label + var tickLabel = document.createElement("label"); + tickLabel.setAttribute( "for", tickBox.id ); + tickLabel.setAttribute( "class", "inline" ); + tickLabel.appendChild( document.createTextNode( dwv.i18n("basics.editMode") ) ); + // checkbox div + var tickDiv = document.createElement("div"); + tickDiv.appendChild(tickLabel); + tickDiv.appendChild(tickBox); + + // search form + node.appendChild(dwv.html.getHtmlSearchForm(table)); + // tick form + node.appendChild(tickDiv); + + // draw list table + node.appendChild(table); + + // delete draw button + var deleteButton = document.createElement("button"); + deleteButton.onclick = function () { app.deleteDraws(); }; + deleteButton.setAttribute( "class", "ui-btn ui-btn-inline" ); + deleteButton.appendChild( document.createTextNode( dwv.i18n("basics.deleteDraws") ) ); + if (!isEditable) { + deleteButton.style.display = "none"; + } + node.appendChild(deleteButton); + + // refresh + dwv.gui.refreshElement(node); + }; + +}; // class dwv.gui.base.DrawList + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; + +/** + * Append the version HTML. + */ +dwv.gui.base.appendVersionHtml = function (version) +{ + var nodes = document.getElementsByClassName("dwv-version"); + if ( nodes ) { + for( var i = 0; i < nodes.length; ++i ){ + nodes[i].appendChild( document.createTextNode(version) ); + } + } +}; + +/** + * Build the help HTML. + * @param {Boolean} mobile Flag for mobile or not environement. + */ +dwv.gui.base.appendHelpHtml = function(toolList, mobile, app) +{ + var actionType = "mouse"; + if( mobile ) { + actionType = "touch"; + } + + var toolHelpDiv = document.createElement("div"); + + var tool = null; + var tkeys = Object.keys(toolList); + for ( var t=0; t < tkeys.length; ++t ) + { + tool = toolList[tkeys[t]]; + // title + var title = document.createElement("h3"); + title.appendChild(document.createTextNode(tool.getHelp().title)); + // doc div + var docDiv = document.createElement("div"); + // brief + var brief = document.createElement("p"); + brief.appendChild(document.createTextNode(tool.getHelp().brief)); + docDiv.appendChild(brief); + // details + if( tool.getHelp()[actionType] ) { + var keys = Object.keys(tool.getHelp()[actionType]); + for( var i=0; i 10 ) { + content = Array.prototype.slice.call( content, 0, 10 ); + content[10] = "..."; + } + str = Array.prototype.join.call( content, ', ' ); + } + // append + cell.appendChild(document.createTextNode(str)); +}; + +/** + * Append a header cell to a given row. + * @param {Object} row The row to append the header cell to. + * @param {String} text The text of the header cell. + */ +dwv.html.appendHCell = function (row, text) +{ + var cell = document.createElement("th"); + cell.appendChild(document.createTextNode(text)); + row.appendChild(cell); +}; + +/** + * Append a row to an array. + * @param {Object} table The HTML table to append a row to. + * @param {Array} input The input row array. + * @param {Number} level The depth level of the input array. + * @param {Number} maxLevel The maximum depth level. + * @param {String} rowHeader The content of the first cell of a row (mainly for objects). + */ +dwv.html.appendRowForArray = function (table, input, level, maxLevel, rowHeader) +{ + var row = null; + // loop through + for ( var i=0; i= maxLevel ) { + if ( !row ) { + row = table.insertRow(-1); + } + dwv.html.appendCell(row, value); + } + // more to come + else { + dwv.html.appendRow(table, value, level+i, maxLevel, rowHeader); + } + } +}; + +/** + * Append a row to an object. + * @param {Object} table The HTML table to append a row to. + * @param {Array} input The input row array. + * @param {Number} level The depth level of the input array. + * @param {Number} maxLevel The maximum depth level. + * @param {String} rowHeader The content of the first cell of a row (mainly for objects). + */ +dwv.html.appendRowForObject = function (table, input, level, maxLevel, rowHeader) +{ + var keys = Object.keys(input); + var row = null; + for ( var o=0; o= maxLevel ) { + if ( !row ) { + row = table.insertRow(-1); + } + if ( o === 0 && rowHeader) { + dwv.html.appendCell(row, rowHeader); + } + dwv.html.appendCell(row, value); + } + // more to come + else { + dwv.html.appendRow(table, value, level+o, maxLevel, keys[o]); + } + } + // header row + // warn: need to create the header after the rest + // otherwise the data will inserted in the thead... + if ( level === 2 ) { + var header = table.createTHead(); + var th = header.insertRow(-1); + if ( rowHeader ) { + dwv.html.appendHCell(th, ""); + } + for ( var k=0; k]+>/g, "").toLowerCase(); + if (text.indexOf(terms[i]) < 0) { + display = 'none'; + } else { + if (terms[i].length) { + dwv.html.highlight(terms[i], table.rows[r]); + } + } + table.rows[r].style.display = display; + } + } +}; + +/** + * Transform back each + * 'preText term postText' + * into its original 'preText term postText'. + * @param {Object} container The container to de-highlight. + */ +dwv.html.dehighlight = function (container) { + for (var i = 0; i < container.childNodes.length; i++) { + var node = container.childNodes[i]; + + if (node.attributes && + node.attributes['class'] && + node.attributes['class'].value === 'highlighted') { + node.parentNode.parentNode.replaceChild( + document.createTextNode( + node.parentNode.innerHTML.replace(/<[^>]+>/g, "")), + node.parentNode); + // Stop here and process next parent + return; + } else if (node.nodeType !== 3) { + // Keep going onto other elements + dwv.html.dehighlight(node); + } + } +}; + +/** + * Create a + * 'preText term postText' + * around each search term. + * @param {String} term The term to highlight. + * @param {Object} container The container where to highlight the term. + */ +dwv.html.highlight = function (term, container) { + for (var i = 0; i < container.childNodes.length; i++) { + var node = container.childNodes[i]; + + if (node.nodeType === 3) { + // Text node + var data = node.data; + var data_low = data.toLowerCase(); + if (data_low.indexOf(term) >= 0) { + //term found! + var new_node = document.createElement('span'); + node.parentNode.replaceChild(new_node, node); + + var result; + while ((result = data_low.indexOf(term)) !== -1) { + // before term + new_node.appendChild(document.createTextNode( + data.substr(0, result))); + // term + new_node.appendChild(dwv.html.createHighlightNode( + document.createTextNode(data.substr( + result, term.length)))); + // reduce search string + data = data.substr(result + term.length); + data_low = data_low.substr(result + term.length); + } + new_node.appendChild(document.createTextNode(data)); + } + } else { + // Keep going onto other elements + dwv.html.highlight(term, node); + } + } +}; + +/** + * Highlight a HTML node. + * @param {Object} child The child to highlight. + * @return {Object} The created HTML node. + */ +dwv.html.createHighlightNode = function (child) { + var node = document.createElement('span'); + node.setAttribute('class', 'highlighted'); + node.attributes['class'].value = 'highlighted'; + node.appendChild(child); + return node; +}; + +/** + * Remove all children of a HTML node. + * @param {Object} node The node to remove kids. + */ +dwv.html.cleanNode = function (node) { + // remove its children if node exists + if ( !node ) { + return; + } + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); + } +}; + +/** + * Remove a HTML node and all its children. + * @param {String} nodeId The string id of the node to delete. + */ +dwv.html.removeNode = function (node) { + // check node + if ( !node ) { + return; + } + // remove its children + dwv.html.cleanNode(node); + // remove it from its parent + var top = node.parentNode; + top.removeChild(node); +}; + +/** + * Remove a list of HTML nodes and all their children. + * @param {Array} nodes The list of nodes to delete. + */ +dwv.html.removeNodes = function (nodes) { + for ( var i = 0; i < nodes.length; ++i ) { + dwv.html.removeNode(nodes[i]); + } +}; + +/** + * Translate the content of an HTML row. + * @param {Object} row The HTML row to parse. + * @param {String} i18nPrefix The i18n prefix to use to find the translation. + */ +dwv.html.translateTableRow = function (row, i18nPrefix) { + var prefix = (typeof i18nPrefix === "undefined") ? "basics" : i18nPrefix; + if (prefix.length !== 0) { + prefix += "."; + } + var cells = row.cells; + for (var c = 0; c < cells.length; ++c) { + var text = cells[c].firstChild.data; + cells[c].firstChild.data = dwv.i18n( prefix + text ); + } +}; + +/** + * Translate the content of an HTML column. + * @param {Object} table The HTML table to parse. + * @param {Number} columnNumber The number of the column to translate. + * @param {String} i18nPrefix The i18n prefix to use to find the translation. + * @param {String} i18nSuffix The i18n suffix to use to find the translation. + */ +dwv.html.translateTableColumn = function (table, columnNumber, i18nPrefix, i18nSuffix) { + var prefix = (typeof i18nPrefix === "undefined") ? "basics" : i18nPrefix; + if (prefix.length !== 0) { + prefix += "."; + } + var suffix = (typeof i18nSuffix === "undefined") ? "" : i18nSuffix; + if (suffix.length !== 0) { + suffix = "." + suffix; + } + if (table.rows.length !== 0) { + for (var r = 1; r < table.rows.length; ++r) { + var cells = table.rows.item(r).cells; + if (cells.length >= columnNumber) { + var text = cells[columnNumber].firstChild.data; + cells[columnNumber].firstChild.data = dwv.i18n( prefix + text + suffix ); + } + } + } +}; + +/** + * Make a HTML table cell editable by putting its content inside an input element. + * @param {Object} cell The cell to make editable. + * @param {Function} onchange The callback to call when cell's content is changed. + * if set to null, the HTML input will be disabled. + * @param {String} inputType The type of the HTML input, default to 'text'. + */ +dwv.html.makeCellEditable = function (cell, onchange, inputType) { + // check event + if (typeof cell === "undefined" ) { + console.warn("Cannot create input for non existing cell."); + return; + } + // HTML input + var input = document.createElement("input"); + // handle change + if (onchange) { + input.onchange = onchange; + } + else { + input.disabled = true; + } + // set input value + input.value = cell.firstChild.data; + // input type + if (typeof inputType === "undefined" || + (inputType === "color" && !dwv.browser.hasInputColor() ) ) { + input.type = "text"; + } + else { + input.type = inputType; + } + + // clean cell + dwv.html.cleanNode(cell); + + // HTML form + var form = document.createElement("form"); + form.onsubmit = function (event) { + event.preventDefault(); + }; + form.appendChild(input); + // add form to cell + cell.appendChild(form); +}; + +/** + * Set the document cursor to 'pointer'. + */ +dwv.html.setCursorToPointer = function () { + document.body.style.cursor = 'pointer'; +}; + +/** + * Set the document cursor to 'default'. + */ +dwv.html.setCursorToDefault = function () { + document.body.style.cursor = 'default'; +}; + + +/** + * Create a HTML select from an input array of options. + * The values of the options are the name of the option made lower case. + * It is left to the user to set the 'onchange' method of the select. + * @param {String} name The name of the HTML select. + * @param {Mixed} list The list of options of the HTML select. + * @param {String} i18nPrefix An optional namespace prefix to find the translation values. + * @param {Bool} i18nSafe An optional flag to check translation existence. + * @return {Object} The created HTML select. + */ +dwv.html.createHtmlSelect = function (name, list, i18nPrefix, i18nSafe) { + // select + var select = document.createElement("select"); + //select.name = name; + select.className = name; + var prefix = (typeof i18nPrefix === "undefined") ? "" : i18nPrefix + "."; + var safe = (typeof i18nSafe === "undefined") ? false : true; + var getText = function(value) { + var key = prefix + value + ".name"; + var text = ""; + if (safe) { + if (dwv.i18nExists(key)) { + text = dwv.i18n(key); + } + else { + text = value; + } + } + else { + text = dwv.i18n(key); + } + return text; + }; + // options + var option; + if ( list instanceof Array ) + { + for ( var i in list ) { + if ( list.hasOwnProperty(i) ) { + option = document.createElement("option"); + option.value = list[i]; + option.appendChild(document.createTextNode(getText(list[i]))); + select.appendChild(option); + } + } + } + else if ( typeof list === 'object') + { + for ( var item in list ) + { + option = document.createElement("option"); + option.value = item; + option.appendChild(document.createTextNode(getText(item))); + select.appendChild(option); + } + } + else + { + throw new Error("Unsupported input list type."); + } + return select; +}; + +/** + * Display or not an element. + * @param {Object} element The HTML element to display. + * @param {Boolean} flag True to display the element. + */ +dwv.html.displayElement = function (element, flag) +{ + element.style.display = flag ? "" : "none"; +}; + +/** + * Toggle the display of an element. + * @param {Object} element The HTML element to display. + */ +dwv.html.toggleDisplay = function (element) +{ + if ( element.style.display === "none" ) { + element.style.display = ''; + } + else { + element.style.display = "none"; + } +}; + +/** + * Append an element. + * @param {Object} parent The HTML element to append to. + * @param {Object} element The HTML element to append. + */ +dwv.html.appendElement = function (parent, element) +{ + // append + parent.appendChild(element); + // refresh + dwv.gui.refreshElement(parent); +}; + +/** + * Create an element. + * @param {String} type The type of the elemnt. + * @param {String} className The className of the element. + */ +dwv.html.createHiddenElement = function (type, className) +{ + var element = document.createElement(type); + element.className = className; + // hide by default + element.style.display = "none"; + // return + return element; +}; + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; +dwv.gui.info = dwv.gui.info || {}; + +/** + * Plot some data in a given div. + * @param {Object} div The HTML element to add WindowLevel info to. + * @param {Array} data The data array to plot. + * @param {Object} options Plot options. + */ +dwv.gui.base.plot = function (/*div, data, options*/) +{ + // default does nothing... +}; + +/** + * MiniColourMap info layer. + * @constructor + * @param {Object} div The HTML element to add colourMap info to. + * @param {Object} app The associated application. + */ +dwv.gui.info.MiniColourMap = function ( div, app ) +{ + /** + * Create the mini colour map info div. + */ + this.create = function () + { + // clean div + var elems = div.getElementsByClassName("colour-map-info"); + if ( elems.length !== 0 ) { + dwv.html.removeNodes(elems); + } + // colour map + var canvas = document.createElement("canvas"); + canvas.className = "colour-map-info"; + canvas.width = 98; + canvas.height = 10; + // add canvas to div + div.appendChild(canvas); + }; + + /** + * Update the mini colour map info div. + * @param {Object} event The windowing change event containing the new values. + * Warning: expects the mini colour map div to exist (use after createMiniColourMap). + */ + this.update = function (event) + { + var windowCenter = event.wc; + var windowWidth = event.ww; + // retrieve canvas and context + var canvas = div.getElementsByClassName("colour-map-info")[0]; + var context = canvas.getContext('2d'); + // fill in the image data + var colourMap = app.getViewController().getColourMap(); + var imageData = context.getImageData(0,0,canvas.width, canvas.height); + // histogram sampling + var c = 0; + var minInt = app.getImage().getRescaledDataRange().min; + var range = app.getImage().getRescaledDataRange().max - minInt; + var incrC = range / canvas.width; + // Y scale + var y = 0; + var yMax = 255; + var yMin = 0; + // X scale + var xMin = windowCenter - 0.5 - (windowWidth-1) / 2; + var xMax = windowCenter - 0.5 + (windowWidth-1) / 2; + // loop through values + var index; + for ( var j = 0; j < canvas.height; ++j ) { + c = minInt; + for ( var i = 0; i < canvas.width; ++i ) { + if ( c <= xMin ) { + y = yMin; + } + else if ( c > xMax ) { + y = yMax; + } + else { + y = ( (c - (windowCenter-0.5) ) / (windowWidth-1) + 0.5 ) * + (yMax-yMin) + yMin; + y = parseInt(y,10); + } + index = (i + j * canvas.width) * 4; + imageData.data[index] = colourMap.red[y]; + imageData.data[index+1] = colourMap.green[y]; + imageData.data[index+2] = colourMap.blue[y]; + imageData.data[index+3] = 0xff; + c += incrC; + } + } + // put the image data in the context + context.putImageData(imageData, 0, 0); + }; +}; // class dwv.gui.info.MiniColourMap + + +/** + * Plot info layer. + * @constructor + * @param {Object} div The HTML element to add colourMap info to. + * @param {Object} app The associated application. + */ +dwv.gui.info.Plot = function (div, app) +{ + /** + * Create the plot info. + */ + this.create = function() + { + // clean div + if ( div ) { + dwv.html.cleanNode(div); + } + // plot + dwv.gui.plot(div, app.getImage().getHistogram()); + }; + + /** + * Update plot. + * @param {Object} event The windowing change event containing the new values. + * Warning: expects the plot to exist (use after createPlot). + */ + this.update = function (event) + { + var wc = event.wc; + var ww = event.ww; + + var half = parseInt( (ww-1) / 2, 10 ); + var center = parseInt( (wc-0.5), 10 ); + var min = center - half; + var max = center + half; + + var markings = [ + { "color": "#faa", "lineWidth": 1, "xaxis": { "from": min, "to": min } }, + { "color": "#aaf", "lineWidth": 1, "xaxis": { "from": max, "to": max } } + ]; + + // plot + dwv.gui.plot(div, app.getImage().getHistogram(), {markings: markings}); + }; + +}; // class dwv.gui.info.Plot + +/** + * DICOM Header overlay info layer. + * @constructor + * @param {Object} div The HTML element to add Header overlay info to. + * @param {String} pos The string to specify the corner position. (tl,tc,tr,cl,cr,bl,bc,br) + */ +dwv.gui.info.Overlay = function ( div, pos, app ) +{ + // closure to self + var self = this; + + /** + * Get the overlay array of the current position. + * @return {Array} The overlay information. + */ + this.getOverlays = function () + { + var image = app.getImage(); + if (!image) { + return; + } + var allOverlays = image.getOverlays(); + if (!allOverlays) { + return; + } + var position = app.getViewController().getCurrentPosition(); + var sliceOverlays = allOverlays[position.k]; + if (!sliceOverlays) { + return; + } + return sliceOverlays[pos]; + }; + + /** + * Create the overlay info div. + */ + this.create = function () + { + // remove all
    elements from div + dwv.html.cleanNode(div); + + // get overlay string array of the current position + var overlays = self.getOverlays(); + if (!overlays) { + return; + } + + if (pos === "bc" || pos === "tc" || + pos === "cr" || pos === "cl") { + div.textContent = overlays[0].value; + } else { + // create
      element + var ul = document.createElement("ul"); + + for (var n=0; overlays[n]; n++){ + var li; + if (overlays[n].value === "window-center") { + li = document.createElement("li"); + li.className = "info-" + pos + "-window-center"; + ul.appendChild(li); + } else if (overlays[n].value === "window-width") { + li = document.createElement("li"); + li.className = "info-" + pos + "-window-width"; + ul.appendChild(li); + } else if (overlays[n].value === "zoom") { + li = document.createElement("li"); + li.className = "info-" + pos + "-zoom"; + ul.appendChild(li); + } else if (overlays[n].value === "offset") { + li = document.createElement("li"); + li.className = "info-" + pos + "-offset"; + ul.appendChild(li); + } else if (overlays[n].value === "value") { + li = document.createElement("li"); + li.className = "info-" + pos + "-value"; + ul.appendChild(li); + } else if (overlays[n].value === "position") { + li = document.createElement("li"); + li.className = "info-" + pos + "-position"; + ul.appendChild(li); + } else if (overlays[n].value === "frame") { + li = document.createElement("li"); + li.className = "info-" + pos + "-frame"; + ul.appendChild(li); + } else { + li = document.createElement("li"); + li.className = "info-" + pos + "-" + n; + li.appendChild( document.createTextNode( overlays[n].value ) ); + ul.appendChild(li); + } + } + + // append
        element before color map + div.appendChild(ul); + } + }; + + /** + * Update the overlay info div. + * @param {Object} event A change event. + */ + this.update = function ( event ) + { + // get overlay string array of the current position + var overlays = self.getOverlays(); + if (!overlays) { + return; + } + + if (pos === "bc" || pos === "tc" || + pos === "cr" || pos === "cl") { + div.textContent = overlays[0].value; + } else { + var li; + var n; + + for (n=0; overlays[n]; n++) { + if (overlays[n].value === "window-center") { + if (event.type === "wl-center-change") { + li = div.getElementsByClassName("info-" + pos + "-window-center")[0]; + dwv.html.cleanNode(li); + var wcStr = dwv.utils.replaceFlags2( overlays[n].format, [Math.round(event.wc)] ); + if (li) { + li.appendChild( document.createTextNode(wcStr) ); + } + } + } else if (overlays[n].value === "window-width") { + if (event.type === "wl-width-change") { + li = div.getElementsByClassName("info-" + pos + "-window-width")[0]; + dwv.html.cleanNode(li); + var wwStr = dwv.utils.replaceFlags2( overlays[n].format, [Math.round(event.ww)] ); + if (li) { + li.appendChild( document.createTextNode(wwStr) ); + } + } + } else if (overlays[n].value === "zoom") { + if (event.type === "zoom-change") { + li = div.getElementsByClassName("info-" + pos + "-zoom")[0]; + dwv.html.cleanNode(li); + var zoom = Number(event.scale).toPrecision(3); + var zoomStr = dwv.utils.replaceFlags2( overlays[n].format, [zoom] ); + if (li) { + li.appendChild( document.createTextNode( zoomStr ) ); + } + } + } else if (overlays[n].value === "offset") { + if (event.type === "zoom-change") { + li = div.getElementsByClassName("info-" + pos + "-offset")[0]; + dwv.html.cleanNode(li); + var offset = [ Number(event.cx).toPrecision(3), + Number(event.cy).toPrecision(3)]; + var offStr = dwv.utils.replaceFlags2( overlays[n].format, offset ); + if (li) { + li.appendChild( document.createTextNode( offStr ) ); + } + } + } else if (overlays[n].value === "value") { + if (event.type === "position-change") { + li = div.getElementsByClassName("info-" + pos + "-value")[0]; + dwv.html.cleanNode(li); + var valueStr = dwv.utils.replaceFlags2( overlays[n].format, [event.value] ); + if (li) { + li.appendChild( document.createTextNode( valueStr ) ); + } + } + } else if (overlays[n].value === "position") { + if (event.type === "position-change") { + li = div.getElementsByClassName("info-" + pos + "-position")[0]; + dwv.html.cleanNode(li); + var posStr = dwv.utils.replaceFlags2( overlays[n].format, [event.i, event.j, event.k] ); + if (li) { + li.appendChild( document.createTextNode( posStr ) ); + } + } + } else if (overlays[n].value === "frame") { + if (event.type === "frame-change") { + li = div.getElementsByClassName("info-" + pos + "-frame")[0]; + dwv.html.cleanNode(li); + var frameStr = dwv.utils.replaceFlags2( overlays[n].format, [event.frame] ); + if (li) { + li.appendChild( document.createTextNode( frameStr ) ); + } + } + } else { + if (event.type === "position-change") { + li = div.getElementsByClassName("info-" + pos + "-" + n)[0]; + dwv.html.cleanNode(li); + if (li) { + li.appendChild( document.createTextNode( overlays[n].value ) ); + } + } + } + } + } + }; +}; // class dwv.gui.info.Overlay + +/** + * Create overlay string array of the image in each corner + * @param {Object} dicomElements DICOM elements of the image + * @return {Array} Array of string to be shown in each corner + */ +dwv.gui.info.createOverlays = function (dicomElements) +{ + var overlays = {}; + var modality = dicomElements.getFromKey("x00080060"); + if (!modality){ + return overlays; + } + + var omaps = dwv.gui.info.overlayMaps; + if (!omaps){ + return overlays; + } + var omap = omaps[modality] || omaps['*']; + + for (var n=0; omap[n]; n++){ + var value = omap[n].value; + var tags = omap[n].tags; + var format = omap[n].format; + var pos = omap[n].pos; + + if (typeof tags !== "undefined" && tags.length !== 0) { + // get values + var values = []; + for ( var i = 0; i < tags.length; ++i ) { + values.push( dicomElements.getElementValueAsStringFromKey( tags[i] ) ); + } + // format + if (typeof format === "undefined" || format === null) { + format = dwv.utils.createDefaultReplaceFormat( values ); + } + value = dwv.utils.replaceFlags2( format, values ); + } + + if (!value || value.length === 0){ + continue; + } + + // add value to overlays + if (!overlays[pos]) { + overlays[pos] = []; + } + overlays[pos].push({'value': value.trim(), 'format': format}); + } + + // (0020,0020) Patient Orientation + var valuePO = dicomElements.getFromKey("x00200020"); + if (typeof valuePO !== "undefined" && valuePO !== null && valuePO.length == 2){ + var po0 = dwv.dicom.cleanString(valuePO[0]); + var po1 = dwv.dicom.cleanString(valuePO[1]); + overlays.cr = [{'value': po0}]; + overlays.cl = [{'value': dwv.dicom.getReverseOrientation(po0)}]; + overlays.bc = [{'value': po1}]; + overlays.tc = [{'value': dwv.dicom.getReverseOrientation(po1)}]; + } + + return overlays; +}; + +/** + * Create overlay string array of the image in each corner + * @param {Object} dicomElements DICOM elements of the image + * @return {Array} Array of string to be shown in each corner + */ +dwv.gui.info.createOverlaysForDom = function (info) +{ + var overlays = {}; + var omaps = dwv.gui.info.overlayMaps; + if (!omaps){ + return overlays; + } + var omap = omaps.DOM; + if (!omap){ + return overlays; + } + + for (var n=0; omap[n]; n++){ + var value = omap[n].value; + var tags = omap[n].tags; + var format = omap[n].format; + var pos = omap[n].pos; + + if (typeof tags !== "undefined" && tags.length !== 0) { + // get values + var values = []; + for ( var i = 0; i < tags.length; ++i ) { + for ( var j = 0; j < info.length; ++j ) { + if (tags[i] === info[j].name) { + values.push( info[j].value ); + } + } + } + // format + if (typeof format === "undefined" || format === null) { + format = dwv.utils.createDefaultReplaceFormat( values ); + } + value = dwv.utils.replaceFlags2( format, values ); + } + + if (!value || value.length === 0){ + continue; + } + + // add value to overlays + if (!overlays[pos]) { + overlays[pos] = []; + } + overlays[pos].push({'value': value.trim(), 'format': format}); + } + + return overlays; +}; + +// namespaces +var dwv = dwv || {}; +dwv.html = dwv.html || {}; + +/** + * Window layer. + * @constructor + * @param {String} name The name of the layer. + */ +dwv.html.Layer = function(canvas) +{ + /** + * A cache of the initial canvas. + * @private + * @type Object + */ + var cacheCanvas = null; + /** + * The associated CanvasRenderingContext2D. + * @private + * @type Object + */ + var context = null; + + /** + * Get the layer canvas. + * @return {Object} The layer canvas. + */ + this.getCanvas = function() { return canvas; }; + /** + * Get the layer context. + * @return {Object} The layer context. + */ + this.getContext = function() { return context; }; + /** + * Get the layer offset on page. + * @return {Number} The layer offset on page. + */ + this.getOffset = function() { return canvas.offset(); }; + + /** + * The image data array. + * @private + * @type Array + */ + var imageData = null; + + /** + * The layer origin. + * @private + * @type {Object} + */ + var origin = {'x': 0, 'y': 0}; + /** + * Get the layer origin. + * @return {Object} The layer origin as {'x','y'}. + */ + this.getOrigin = function () { + return origin; + }; + /** + * The layer zoom. + * @private + * @type {Object} + */ + var zoom = {'x': 1, 'y': 1}; + /** + * Get the layer zoom. + * @return {Object} The layer zoom as {'x','y'}. + */ + this.getZoom = function () { + return zoom; + }; + + /** + * The layer translation. + * @private + * @type {Object} + */ + var trans = {'x': 0, 'y': 0}; + /** + * Get the layer translation. + * @return {Object} The layer translation as {'x','y'}. + */ + this.getTrans = function () { + return trans; + }; + + /** + * Set the canvas width. + * @param {Number} width The new width. + */ + this.setWidth = function ( width ) { + canvas.width = width; + }; + /** + * Set the canvas height. + * @param {Number} height The new height. + */ + this.setHeight = function ( height ) { + canvas.height = height; + }; + + /** + * Set the layer zoom. + * @param {Number} newZoomX The zoom in the X direction. + * @param {Number} newZoomY The zoom in the Y direction. + * @param {Number} centerX The zoom center in the X direction. + * @param {Number} centerY The zoom center in the Y direction. + */ + this.zoom = function(newZoomX,newZoomY,centerX,centerY) + { + // The zoom is the ratio between the differences from the center + // to the origins: + // centerX - originX = ( centerX - originX0 ) * zoomX + // (center in ~world coordinate system) + //originX = (centerX / zoomX) + originX - (centerX / newZoomX); + //originY = (centerY / zoomY) + originY - (centerY / newZoomY); + + // center in image coordinate system + origin.x = centerX - (centerX - origin.x) * (newZoomX / zoom.x); + origin.y = centerY - (centerY - origin.y) * (newZoomY / zoom.y); + + // save zoom + zoom.x = newZoomX; + zoom.y = newZoomY; + }; + + /** + * Set the layer translation. + * Translation is according to the last one. + * @param {Number} tx The translation in the X direction. + * @param {Number} ty The translation in the Y direction. + */ + this.translate = function(tx,ty) + { + trans.x = tx; + trans.y = ty; + }; + + /** + * Set the image data array. + * @param {Array} data The data array. + */ + this.setImageData = function(data) + { + imageData = data; + // update the cached canvas + cacheCanvas.getContext("2d").putImageData(imageData, 0, 0); + }; + + /** + * Reset the layout. + */ + this.resetLayout = function(izoom) + { + origin.x = 0; + origin.y = 0; + zoom.x = izoom; + zoom.y = izoom; + trans.x = 0; + trans.y = 0; + }; + + /** + * Transform a display position to an index. + */ + this.displayToIndex = function ( point2D ) { + return {'x': ( (point2D.x - origin.x) / zoom.x ) - trans.x, + 'y': ( (point2D.y - origin.y) / zoom.y ) - trans.y}; + }; + + /** + * Draw the content (imageData) of the layer. + * The imageData variable needs to be set + */ + this.draw = function () + { + // clear the context: reset the transform first + // store the current transformation matrix + context.save(); + // use the identity matrix while clearing the canvas + context.setTransform( 1, 0, 0, 1, 0, 0 ); + context.clearRect( 0, 0, canvas.width, canvas.height ); + // restore the transform + context.restore(); + + // draw the cached canvas on the context + // transform takes as input a, b, c, d, e, f to create + // the transform matrix (column-major order): + // [ a c e ] + // [ b d f ] + // [ 0 0 1 ] + context.setTransform( zoom.x, 0, 0, zoom.y, + origin.x + (trans.x * zoom.x), + origin.y + (trans.y * zoom.y) ); + context.drawImage( cacheCanvas, 0, 0 ); + }; + + /** + * Initialise the layer: set the canvas and context + * @input {Number} inputWidth The width of the canvas. + * @input {Number} inputHeight The height of the canvas. + */ + this.initialise = function(inputWidth, inputHeight) + { + // find the canvas element + //canvas = document.getElementById(name); + //if (!canvas) + //{ + // alert("Error: cannot find the canvas element for '" + name + "'."); + // return; + //} + // check that the getContext method exists + if (!canvas.getContext) + { + alert("Error: no canvas.getContext method."); + return; + } + // get the 2D context + context = canvas.getContext('2d'); + if (!context) + { + alert("Error: failed to get the 2D context."); + return; + } + // canvas sizes + canvas.width = inputWidth; + canvas.height = inputHeight; + // original empty image data array + context.clearRect (0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + // cached canvas + cacheCanvas = document.createElement("canvas"); + cacheCanvas.width = inputWidth; + cacheCanvas.height = inputHeight; + }; + + /** + * Fill the full context with the current style. + */ + this.fillContext = function() + { + context.fillRect( 0, 0, canvas.width, canvas.height ); + }; + + /** + * Clear the context and reset the image data. + */ + this.clear = function() + { + context.clearRect(0, 0, canvas.width, canvas.height); + imageData = context.getImageData(0, 0, canvas.width, canvas.height); + this.resetLayout(); + }; + + /** + * Merge two layers. + * @input {Layer} layerToMerge The layer to merge. It will also be emptied. + */ + this.merge = function(layerToMerge) + { + // basic resampling of the merge data to put it at zoom 1:1 + var mergeImageData = layerToMerge.getContext().getImageData( + 0, 0, canvas.width, canvas.height); + var offMerge = 0; + var offMergeJ = 0; + var offThis = 0; + var offThisJ = 0; + var alpha = 0; + for( var j=0; j < canvas.height; ++j ) { + offMergeJ = parseInt( (origin.y + j * zoom.y), 10 ) * canvas.width; + offThisJ = j * canvas.width; + for( var i=0; i < canvas.width; ++i ) { + // 4 component data: RGB + alpha + offMerge = 4 * ( parseInt( (origin.x + i * zoom.x), 10 ) + offMergeJ ); + offThis = 4 * ( i + offThisJ ); + // merge non transparent + alpha = mergeImageData.data[offMerge+3]; + if( alpha !== 0 ) { + imageData.data[offThis] = mergeImageData.data[offMerge]; + imageData.data[offThis+1] = mergeImageData.data[offMerge+1]; + imageData.data[offThis+2] = mergeImageData.data[offMerge+2]; + imageData.data[offThis+3] = alpha; + } + } + } + // empty and reset merged layer + layerToMerge.clear(); + // draw the layer + this.draw(); + }; + + /** + * Set the line colour for the layer. + * @input {String} colour The line colour. + */ + this.setLineColour = function(colour) + { + context.fillStyle = colour; + context.strokeStyle = colour; + }; + + /** + * Display the layer. + * @input {Boolean} val Whether to display the layer or not. + */ + this.setStyleDisplay = function(val) + { + if( val === true ) + { + canvas.style.display = ''; + } + else + { + canvas.style.display = "none"; + } + }; + + /** + * Check if the layer is visible. + * @return {Boolean} True if the layer is visible. + */ + this.isVisible = function() + { + if( canvas.style.display === "none" ) { + return false; + } + else { + return true; + } + }; + + /** + * Align on another layer. + * @param {Layer} rhs The layer to align on. + */ + this.align = function(rhs) + { + canvas.style.top = rhs.getCanvas().offsetTop; + canvas.style.left = rhs.getCanvas().offsetLeft; + }; +}; // Layer class + +/** + * Get the offset of an input event. + * @param {Object} event The event to get the offset from. + * @return {Array} The array of offsets. + */ +dwv.html.getEventOffset = function (event) { + var positions = []; + var ex = 0; + var ey = 0; + if ( event.targetTouches ) { + // get the touch offset from all its parents + var offsetLeft = 0; + var offsetTop = 0; + var offsetParent = event.targetTouches[0].target.offsetParent; + while ( offsetParent ) { + if (!isNaN(offsetParent.offsetLeft)) { + offsetLeft += offsetParent.offsetLeft; + } + if (!isNaN(offsetParent.offsetTop)) { + offsetTop += offsetParent.offsetTop; + } + offsetParent = offsetParent.offsetParent; + } + // set its position + var touch = null; + for ( var i = 0 ; i < event.targetTouches.length; ++i ) { + touch = event.targetTouches[i]; + ex = touch.pageX - offsetLeft; + ey = touch.pageY - offsetTop; + positions.push({'x': ex, 'y': ey}); + } + } + else { + // layerX is used by Firefox + ex = event.offsetX === undefined ? event.layerX : event.offsetX; + ey = event.offsetY === undefined ? event.layerY : event.offsetY; + positions.push({'x': ex, 'y': ey}); + } + return positions; +}; + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; + +/** + * Loadbox base gui. + * @constructor + */ +dwv.gui.base.Loadbox = function (app, loaders) +{ + /** + * Loader HTML select. + * @private + */ + var loaderSelector = null; + + /** + * Setup the loadbox HTML. + */ + this.setup = function () + { + // loader select + loaderSelector = dwv.html.createHtmlSelect("loaderSelect", loaders, "io"); + loaderSelector.onchange = app.onChangeLoader; + + // node + var node = app.getElement("loaderlist"); + // clear it + while(node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + // append + node.appendChild(loaderSelector); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display a loader. + * @param {String} name The name of the loader to show. + */ + this.displayLoader = function (name) + { + var keys = Object.keys(loaders); + for ( var i = 0; i < keys.length; ++i ) { + if ( keys[i] === name ) { + loaders[keys[i]].display(true); + } + else { + loaders[keys[i]].display(false); + } + } + }; + + /** + * Reset to its original state. + */ + this.reset = function () + { + // display first loader + var keys = Object.keys(loaders); + this.displayLoader(keys[0]); + // reset HTML select + if (loaderSelector) { + loaderSelector.selectedIndex = 0; + } + }; + +}; // class dwv.gui.base.Loadbox + +/** + * FileLoad base gui. + * @constructor + */ +dwv.gui.base.FileLoad = function (app) +{ + // closure to self + var self = this; + + /** + * Internal file input change handler. + * @param {Object} event The change event. + */ + function onchangeinternal(event) { + if (typeof self.onchange === "function") { + self.onchange(event); + } + app.onChangeFiles(event); + } + + /** + * Setup the file load HTML to the page. + */ + this.setup = function() + { + // input + var fileLoadInput = document.createElement("input"); + fileLoadInput.onchange = onchangeinternal; + fileLoadInput.type = "file"; + fileLoadInput.multiple = true; + fileLoadInput.className = "imagefiles"; + fileLoadInput.setAttribute("data-clear-btn","true"); + fileLoadInput.setAttribute("data-mini","true"); + + // associated div + var fileLoadDiv = document.createElement("div"); + fileLoadDiv.className = "imagefilesdiv"; + fileLoadDiv.style.display = "none"; + fileLoadDiv.appendChild(fileLoadInput); + + // node + var node = app.getElement("loaderlist"); + // append + node.appendChild(fileLoadDiv); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the file load HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // file div element + var node = app.getElement("loaderlist"); + var filediv = node.getElementsByClassName("imagefilesdiv")[0]; + filediv.style.display = bool ? "" : "none"; + }; + +}; // class dwv.gui.base.FileLoad + +/** + * FolderLoad base gui. + * @constructor + */ +dwv.gui.base.FolderLoad = function (app) +{ + // closure to self + var self = this; + + /** + * Internal file input change handler. + * @param {Object} event The change event. + */ + function onchangeinternal(event) { + if (typeof self.onchange === "function") { + self.onchange(event); + } + app.onChangeFiles(event); + } + + /** + * Setup the file load HTML to the page. + */ + this.setup = function() + { + // input + var fileLoadInput = document.createElement("input"); + fileLoadInput.onchange = onchangeinternal; + fileLoadInput.type = "file"; + fileLoadInput.multiple = true; + fileLoadInput.webkitdirectory = true; + fileLoadInput.className = "imagefolder"; + fileLoadInput.setAttribute("data-clear-btn","true"); + fileLoadInput.setAttribute("data-mini","true"); + + // associated div + var folderLoadDiv = document.createElement("div"); + folderLoadDiv.className = "imagefolderdiv"; + folderLoadDiv.style.display = "none"; + folderLoadDiv.appendChild(fileLoadInput); + + // node + var node = app.getElement("loaderlist"); + // append + node.appendChild(folderLoadDiv); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the folder load HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // file div element + var node = app.getElement("loaderlist"); + var folderdiv = node.getElementsByClassName("imagefolderdiv")[0]; + folderdiv.style.display = bool ? "" : "none"; + }; + +}; // class dwv.gui.base.FileLoad + +/** + * UrlLoad base gui. + * @constructor + */ +dwv.gui.base.UrlLoad = function (app) +{ + // closure to self + var self = this; + + /** + * Internal url input change handler. + * @param {Object} event The change event. + */ + function onchangeinternal(event) { + if (typeof self.onchange === "function") { + self.onchange(event); + } + app.onChangeURL(event); + } + + /** + * Setup the url load HTML to the page. + */ + this.setup = function () + { + // input + var urlLoadInput = document.createElement("input"); + urlLoadInput.onchange = onchangeinternal; + urlLoadInput.type = "url"; + urlLoadInput.className = "imageurl"; + urlLoadInput.setAttribute("data-clear-btn","true"); + urlLoadInput.setAttribute("data-mini","true"); + + // associated div + var urlLoadDiv = document.createElement("div"); + urlLoadDiv.className = "imageurldiv"; + urlLoadDiv.style.display = "none"; + urlLoadDiv.appendChild(urlLoadInput); + + // node + var node = app.getElement("loaderlist"); + // append + node.appendChild(urlLoadDiv); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the url load HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // url div element + var node = app.getElement("loaderlist"); + var urldiv = node.getElementsByClassName("imageurldiv")[0]; + urldiv.style.display = bool ? "" : "none"; + }; + +}; // class dwv.gui.base.UrlLoad + +// namespaces +var dwv = dwv || {}; +dwv.html = dwv.html || {}; + +/** + * Style class. + * @constructor + */ +dwv.html.Style = function () +{ + /** + * Font size. + * @private + * @type Number + */ + var fontSize = 12; + /** + * Font family. + * @private + * @type String + */ + var fontFamily = "Verdana"; + /** + * Text colour. + * @private + * @type String + */ + var textColour = "#fff"; + /** + * Line colour. + * @private + * @type String + */ + var lineColour = "#ffff80"; + /** + * Display scale. + * @private + * @type Number + */ + var displayScale = 1; + /** + * Stroke width. + * @private + * @type Number + */ + var strokeWidth = 2; + + /** + * Get the font family. + * @return {String} The font family. + */ + this.getFontFamily = function () { return fontFamily; }; + + /** + * Get the font size. + * @return {Number} The font size. + */ + this.getFontSize = function () { return fontSize; }; + + /** + * Get the stroke width. + * @return {Number} The stroke width. + */ + this.getStrokeWidth = function () { return strokeWidth; }; + + /** + * Get the text colour. + * @return {String} The text colour. + */ + this.getTextColour = function () { return textColour; }; + + /** + * Get the line colour. + * @return {String} The line colour. + */ + this.getLineColour = function () { return lineColour; }; + + /** + * Set the line colour. + * @param {String} colour The line colour. + */ + this.setLineColour = function (colour) { lineColour = colour; }; + + /** + * Set the display scale. + * @param {String} scale The display scale. + */ + this.setScale = function (scale) { displayScale = scale; }; + + /** + * Scale an input value. + * @param {Number} value The value to scale. + */ + this.scale = function (value) { return value / displayScale; }; +}; + +/** + * Get the font definition string. + * @return {String} The font definition string. + */ +dwv.html.Style.prototype.getFontStr = function () +{ + return ("normal " + this.getFontSize() + "px sans-serif"); +}; + +/** + * Get the line height. + * @return {Number} The line height. + */ +dwv.html.Style.prototype.getLineHeight = function () +{ + return ( this.getFontSize() + this.getFontSize() / 5 ); +}; + +/** + * Get the font size scaled to the display. + * @return {Number} The scaled font size. + */ +dwv.html.Style.prototype.getScaledFontSize = function () +{ + return this.scale( this.getFontSize() ); +}; + +/** + * Get the stroke width scaled to the display. + * @return {Number} The scaled stroke width. + */ +dwv.html.Style.prototype.getScaledStrokeWidth = function () +{ + return this.scale( this.getStrokeWidth() ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; + +/** + * Toolbox base gui. + * @constructor + */ +dwv.gui.base.Toolbox = function (app) +{ + /** + * Setup the toolbox HTML. + */ + this.setup = function (list) + { + // tool select + var toolSelector = dwv.html.createHtmlSelect("toolSelect", list, "tool"); + toolSelector.onchange = app.onChangeTool; + + // tool list element + var toolLi = document.createElement("li"); + toolLi.className = "toolLi ui-block-a"; + toolLi.style.display = "none"; + toolLi.appendChild(toolSelector); + + // tool ul + var toolUl = document.createElement("ul"); + toolUl.appendChild(toolLi); + toolUl.className = "ui-grid-b"; + + // node + var node = app.getElement("toolList"); + // append + node.appendChild(toolUl); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the toolbox HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // tool list element + var node = app.getElement("toolLi"); + dwv.html.displayElement(node, bool); + }; + + /** + * Initialise the toolbox HTML. + */ + this.initialise = function (displays) + { + // tool select: reset selected option + var toolSelector = app.getElement("toolSelect"); + + // update list + var options = toolSelector.options; + var selectedIndex = -1; + for ( var i = 0; i < options.length; ++i ) { + if ( !displays[i] ) { + options[i].style.display = "none"; + } + else { + if ( selectedIndex === -1 ) { + selectedIndex = i; + } + options[i].style.display = ""; + } + } + toolSelector.selectedIndex = selectedIndex; + + // refresh + dwv.gui.refreshElement(toolSelector); + }; + +}; // dwv.gui.base.Toolbox + +/** + * WindowLevel tool base gui. + * @constructor + */ +dwv.gui.base.WindowLevel = function (app) +{ + /** + * Setup the tool HTML. + */ + this.setup = function () + { + // preset select + var wlSelector = dwv.html.createHtmlSelect("presetSelect", []); + wlSelector.onchange = app.onChangeWindowLevelPreset; + // colour map select + var cmSelector = dwv.html.createHtmlSelect("colourMapSelect", dwv.tool.colourMaps, "colourmap"); + cmSelector.onchange = app.onChangeColourMap; + + // preset list element + var wlLi = document.createElement("li"); + wlLi.className = "wlLi ui-block-b"; + //wlLi.className = "wlLi"; + wlLi.style.display = "none"; + wlLi.appendChild(wlSelector); + // colour map list element + var cmLi = document.createElement("li"); + cmLi.className = "cmLi ui-block-c"; + //cmLi.className = "cmLi"; + cmLi.style.display = "none"; + cmLi.appendChild(cmSelector); + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // append preset + node.appendChild(wlLi); + // append colour map + node.appendChild(cmLi); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the tool HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // presets list element + var node = app.getElement("wlLi"); + dwv.html.displayElement(node, bool); + // colour map list element + node = app.getElement("cmLi"); + dwv.html.displayElement(node, bool); + }; + + /** + * Initialise the tool HTML. + */ + this.initialise = function () + { + // create new preset select + var wlSelector = dwv.html.createHtmlSelect("presetSelect", + app.getViewController().getWindowLevelPresetsNames(), "wl.presets", true); + wlSelector.onchange = app.onChangeWindowLevelPreset; + wlSelector.title = "Select w/l preset."; + + // copy html list + var wlLi = app.getElement("wlLi"); + // clear node + dwv.html.cleanNode(wlLi); + // add children + wlLi.appendChild(wlSelector); + // refresh + dwv.gui.refreshElement(wlLi); + + // colour map select + var cmSelector = app.getElement("colourMapSelect"); + cmSelector.selectedIndex = 0; + // special monochrome1 case + if( app.getImage().getPhotometricInterpretation() === "MONOCHROME1" ) + { + cmSelector.selectedIndex = 1; + } + // refresh + dwv.gui.refreshElement(cmSelector); + }; + +}; // class dwv.gui.base.WindowLevel + +/** + * Draw tool base gui. + * @constructor + */ +dwv.gui.base.Draw = function (app) +{ + // default colours + var colours = [ + "Yellow", "Red", "White", "Green", "Blue", "Lime", "Fuchsia", "Black" + ]; + /** + * Get the default colour. + */ + this.getDefaultColour = function () { + if ( dwv.browser.hasInputColor() ) { + return "#FFFF80"; + } + else { + return colours[0]; + } + }; + + /** + * Setup the tool HTML. + */ + this.setup = function (shapeList) + { + // shape select + var shapeSelector = dwv.html.createHtmlSelect("shapeSelect", shapeList, "shape"); + shapeSelector.onchange = app.onChangeShape; + // colour select + var colourSelector = null; + if ( dwv.browser.hasInputColor() ) { + colourSelector = document.createElement("input"); + colourSelector.className = "colourSelect"; + colourSelector.type = "color"; + colourSelector.value = "#FFFF80"; + } + else { + colourSelector = dwv.html.createHtmlSelect("colourSelect", colours, "colour"); + } + colourSelector.onchange = app.onChangeLineColour; + + // shape list element + var shapeLi = document.createElement("li"); + shapeLi.className = "shapeLi ui-block-c"; + shapeLi.style.display = "none"; + shapeLi.appendChild(shapeSelector); + //shapeLi.setAttribute("class","ui-block-c"); + // colour list element + var colourLi = document.createElement("li"); + colourLi.className = "colourLi ui-block-b"; + colourLi.style.display = "none"; + colourLi.appendChild(colourSelector); + //colourLi.setAttribute("class","ui-block-b"); + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // apend shape + node.appendChild(shapeLi); + // append colour + node.appendChild(colourLi); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the tool HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // colour list element + var node = app.getElement("colourLi"); + dwv.html.displayElement(node, bool); + // shape list element + node = app.getElement("shapeLi"); + dwv.html.displayElement(node, bool); + }; + + /** + * Initialise the tool HTML. + */ + this.initialise = function () + { + // shape select: reset selected option + var shapeSelector = app.getElement("shapeSelect"); + shapeSelector.selectedIndex = 0; + // refresh + dwv.gui.refreshElement(shapeSelector); + + // colour select: reset selected option + var colourSelector = app.getElement("colourSelect"); + if ( !dwv.browser.hasInputColor() ) { + colourSelector.selectedIndex = 0; + } + // refresh + dwv.gui.refreshElement(colourSelector); + }; + +}; // class dwv.gui.base.Draw + +/** + * Base gui for a tool with a colour setting. + * @constructor + */ +dwv.gui.base.ColourTool = function (app, prefix) +{ + // default colours + var colours = [ + "Yellow", "Red", "White", "Green", "Blue", "Lime", "Fuchsia", "Black" + ]; + // colour selector class + var colourSelectClassName = prefix + "ColourSelect"; + // colour selector class + var colourLiClassName = prefix + "ColourLi"; + + /** + * Get the default colour. + */ + this.getDefaultColour = function () { + if ( dwv.browser.hasInputColor() ) { + return "#FFFF80"; + } + else { + return colours[0]; + } + }; + + /** + * Setup the tool HTML. + */ + this.setup = function () + { + // colour select + var colourSelector = null; + if ( dwv.browser.hasInputColor() ) { + colourSelector = document.createElement("input"); + colourSelector.className = colourSelectClassName; + colourSelector.type = "color"; + colourSelector.value = "#FFFF80"; + } + else { + colourSelector = dwv.html.createHtmlSelect(colourSelectClassName, colours, "colour"); + } + colourSelector.onchange = app.onChangeLineColour; + + // colour list element + var colourLi = document.createElement("li"); + colourLi.className = colourLiClassName + " ui-block-b"; + colourLi.style.display = "none"; + //colourLi.setAttribute("class","ui-block-b"); + colourLi.appendChild(colourSelector); + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // apend colour + node.appendChild(colourLi); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the tool HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function (bool) + { + // colour list + var node = app.getElement(colourLiClassName); + dwv.html.displayElement(node, bool); + }; + + /** + * Initialise the tool HTML. + */ + this.initialise = function () + { + var colourSelector = app.getElement(colourSelectClassName); + if ( !dwv.browser.hasInputColor() ) { + colourSelector.selectedIndex = 0; + } + dwv.gui.refreshElement(colourSelector); + }; + +}; // class dwv.gui.base.ColourTool + +/** + * ZoomAndPan tool base gui. + * @constructor + */ +dwv.gui.base.ZoomAndPan = function (app) +{ + /** + * Setup the tool HTML. + */ + this.setup = function() + { + // reset button + var button = document.createElement("button"); + button.className = "zoomResetButton"; + button.name = "zoomResetButton"; + button.onclick = app.onZoomReset; + button.setAttribute("style","width:100%; margin-top:0.5em;"); + button.setAttribute("class","ui-btn ui-btn-b"); + var text = document.createTextNode(dwv.i18n("basics.reset")); + button.appendChild(text); + + // list element + var liElement = document.createElement("li"); + liElement.className = "zoomLi ui-block-c"; + liElement.style.display = "none"; + //liElement.setAttribute("class","ui-block-c"); + liElement.appendChild(button); + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // append element + node.appendChild(liElement); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the tool HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function(bool) + { + // display list element + var node = app.getElement("zoomLi"); + dwv.html.displayElement(node, bool); + }; + +}; // class dwv.gui.base.ZoomAndPan + +/** + * Scroll tool base gui. + * @constructor + */ +dwv.gui.base.Scroll = function (app) +{ + /** + * Setup the tool HTML. + */ + this.setup = function() + { + // list element + var liElement = document.createElement("li"); + liElement.className = "scrollLi ui-block-c"; + liElement.style.display = "none"; + + // node + var node = app.getElement("toolList").getElementsByTagName("ul")[0]; + // append element + node.appendChild(liElement); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Display the tool HTML. + * @param {Boolean} bool True to display, false to hide. + */ + this.display = function(bool) + { + // display list element + var node = app.getElement("scrollLi"); + dwv.html.displayElement(node, bool); + }; + +}; // class dwv.gui.base.Scroll + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; +dwv.gui.base = dwv.gui.base || {}; + +/** + * Undo base gui. + * @constructor + */ +dwv.gui.base.Undo = function (app) +{ + /** + * Setup the undo HTML. + */ + this.setup = function () + { + var paragraph = document.createElement("p"); + paragraph.appendChild(document.createTextNode("History:")); + paragraph.appendChild(document.createElement("br")); + + var select = document.createElement("select"); + select.className = "history_list"; + select.name = "history_list"; + select.multiple = "multiple"; + paragraph.appendChild(select); + + // node + var node = app.getElement("history"); + // clear it + while(node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + // append + node.appendChild(paragraph); + // refresh + dwv.gui.refreshElement(node); + }; + + /** + * Clear the command list of the undo HTML. + */ + this.initialise = function () + { + var select = app.getElement("history_list"); + if ( select && select.length !== 0 ) { + for( var i = select.length - 1; i >= 0; --i) + { + select.remove(i); + } + } + // refresh + dwv.gui.refreshElement(select); + }; + + /** + * Add a command to the undo HTML. + * @param {String} commandName The name of the command to add. + */ + this.addCommandToUndoHtml = function (commandName) + { + var select = app.getElement("history_list"); + // remove undone commands + var count = select.length - (select.selectedIndex+1); + if( count > 0 ) + { + for( var i = 0; i < count; ++i) + { + select.remove(select.length-1); + } + } + // add new option + var option = document.createElement("option"); + option.text = commandName; + option.value = commandName; + select.add(option); + // increment selected index + select.selectedIndex++; + // refresh + dwv.gui.refreshElement(select); + }; + + /** + * Enable the last command of the undo HTML. + * @param {Boolean} enable Flag to enable or disable the command. + */ + this.enableInUndoHtml = function (enable) + { + var select = app.getElement("history_list"); + // enable or not (order is important) + var option; + if( enable ) + { + // increment selected index + select.selectedIndex++; + // enable option + option = select.options[select.selectedIndex]; + option.disabled = false; + } + else + { + // disable option + option = select.options[select.selectedIndex]; + option.disabled = true; + // decrement selected index + select.selectedIndex--; + } + // refresh + dwv.gui.refreshElement(select); + }; + +}; // class dwv.gui.base.Undo + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +// JPEG Baseline +var hasJpegBaselineDecoder = (typeof JpegImage !== "undefined"); +var JpegImage = JpegImage || {}; +// JPEG Lossless +var hasJpegLosslessDecoder = (typeof jpeg !== "undefined") && + (typeof jpeg.lossless !== "undefined"); +var jpeg = jpeg || {}; +jpeg.lossless = jpeg.lossless || {}; +// JPEG 2000 +var hasJpeg2000Decoder = (typeof JpxImage !== "undefined"); +var JpxImage = JpxImage || {}; + +/** + * Asynchronous pixel buffer decoder. + * @param {String} script The path to the decoder script to be used by the web worker. + */ +dwv.image.AsynchPixelBufferDecoder = function (script) +{ + // initialise the thread pool + var pool = new dwv.utils.ThreadPool(15); + pool.init(); + + /** + * Decode a pixel buffer. + * @param {Array} pixelBuffer The pixel buffer. + * @param {Number} bitsAllocated The bits allocated per element in the buffer. + * @param {Boolean} isSigned Is the data signed. + * @param {Function} callback Callback function to handle decoded data. + */ + this.decode = function (pixelBuffer, bitsAllocated, isSigned, callback) { + // (re)set event handler + pool.onpoolworkend = this.ondecodeend; + pool.onworkerend = this.ondecoded; + // create worker task + var workerTask = new dwv.utils.WorkerTask(script, callback, { + 'buffer': pixelBuffer, + 'bitsAllocated': bitsAllocated, + 'isSigned': isSigned } ); + // add it the queue and run it + pool.addWorkerTask(workerTask); + }; + + /** + * Abort decoding. + */ + this.abort = function () { + // abort the thread pool + pool.abort(); + }; +}; + +/** + * Handle a decode end event. + */ +dwv.image.AsynchPixelBufferDecoder.prototype.ondecodeend = function () +{ + // default does nothing. +}; + +/** + * Handle a decode event. + */ +dwv.image.AsynchPixelBufferDecoder.prototype.ondecoded = function () +{ + // default does nothing. +}; + +/** + * Synchronous pixel buffer decoder. + * @param {String} algoName The decompression algorithm name. + */ +dwv.image.SynchPixelBufferDecoder = function (algoName) +{ + /** + * Decode a pixel buffer. + * @param {Array} pixelBuffer The pixel buffer. + * @param {Number} bitsAllocated The bits allocated per element in the buffer. + * @param {Boolean} isSigned Is the data signed. + * @param {Function} callback Callback function to handle decoded data. + * @external jpeg + * @external JpegImage + * @external JpxImage + */ + this.decode = function (pixelBuffer, bitsAllocated, isSigned, callback) { + var decoder = null; + var decodedBuffer = null; + if( algoName === "jpeg-lossless" ) { + if ( !hasJpegLosslessDecoder ) { + throw new Error("No JPEG Lossless decoder provided"); + } + // bytes per element + var bpe = bitsAllocated / 8; + var buf = new Uint8Array( pixelBuffer ); + decoder = new jpeg.lossless.Decoder(); + var decoded = decoder.decode(buf.buffer, 0, buf.buffer.byteLength, bpe); + if (bitsAllocated === 8) { + if (isSigned) { + decodedBuffer = new Int8Array(decoded.buffer); + } + else { + decodedBuffer = new Uint8Array(decoded.buffer); + } + } + else if (bitsAllocated === 16) { + if (isSigned) { + decodedBuffer = new Int16Array(decoded.buffer); + } + else { + decodedBuffer = new Uint16Array(decoded.buffer); + } + } + } + else if ( algoName === "jpeg-baseline" ) { + if ( !hasJpegBaselineDecoder ) { + throw new Error("No JPEG Baseline decoder provided"); + } + decoder = new JpegImage(); + decoder.parse( pixelBuffer ); + decodedBuffer = decoder.getData(decoder.width,decoder.height); + } + else if( algoName === "jpeg2000" ) { + if ( !hasJpeg2000Decoder ) { + throw new Error("No JPEG 2000 decoder provided"); + } + // decompress pixel buffer into Int16 image + decoder = new JpxImage(); + decoder.parse( pixelBuffer ); + // set the pixel buffer + decodedBuffer = decoder.tiles[0].items; + } + // send events + this.ondecoded(); + this.ondecodeend(); + // call callback with decoded buffer as array + callback({data: [decodedBuffer]}); + }; + + /** + * Abort decoding. + */ + this.abort = function () { + // nothing to do in the synchronous case. + }; +}; + +/** + * Handle a decode end event. + */ +dwv.image.SynchPixelBufferDecoder.prototype.ondecodeend = function () +{ + // default does nothing. +}; + +/** + * Handle a decode event. + */ +dwv.image.SynchPixelBufferDecoder.prototype.ondecoded = function () +{ + // default does nothing. +}; + +/** + * Decode a pixel buffer. + * @constructor + * @param {String} algoName The decompression algorithm name. + * If the 'dwv.image.decoderScripts' variable does not contain the desired algorythm, + * the decoder will switch to the synchronous mode. + */ +dwv.image.PixelBufferDecoder = function (algoName) +{ + /** + * Pixel decoder. + * Defined only once. + * @private + * @type Object + */ + var pixelDecoder = null; + + // initialise the asynch decoder (if possible) + if (typeof dwv.image.decoderScripts !== "undefined" && + typeof dwv.image.decoderScripts[algoName] !== "undefined") { + pixelDecoder = new dwv.image.AsynchPixelBufferDecoder(dwv.image.decoderScripts[algoName]); + } else { + pixelDecoder = new dwv.image.SynchPixelBufferDecoder(algoName); + } + + /** + * Get data from an input buffer using a DICOM parser. + * @param {Array} pixelBuffer The input data buffer. + * @param {Number} bitsAllocated The bits allocated per element in the buffer. + * @param {Boolean} isSigned Is the data signed. + * @param {Object} callback The callback on the conversion. + */ + this.decode = function (pixelBuffer, bitsAllocated, isSigned, callback) + { + // set event handler + pixelDecoder.ondecodeend = this.ondecodeend; + pixelDecoder.ondecoded = this.ondecoded; + // decode and call the callback + pixelDecoder.decode(pixelBuffer, bitsAllocated, isSigned, callback); + }; + + /** + * Abort decoding. + */ + this.abort = function () + { + // decoder classes should define an abort + pixelDecoder.abort(); + }; +}; + +/** + * Handle a decode end event. + */ +dwv.image.PixelBufferDecoder.prototype.ondecodeend = function () +{ + // default does nothing. +}; + +/** + * Handle a decode end event. + */ +dwv.image.PixelBufferDecoder.prototype.ondecoded = function () +{ + // default does nothing. +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Create a dwv.image.View from a DICOM buffer. + * @constructor + */ +dwv.image.DicomBufferToView = function () +{ + // closure to self + var self = this; + + /** + * The default character set (optional). + * @private + * @type String + */ + var defaultCharacterSet; + + /** + * Set the default character set. + * param {String} The character set. + */ + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; + }; + + /** + * Pixel buffer decoder. + * Define only once to allow optional asynchronous mode. + * @private + * @type Object + */ + var pixelDecoder = null; + + /** + * Get data from an input buffer using a DICOM parser. + * @param {Array} buffer The input data buffer. + * @param {Number} dataIndex The data index. + */ + this.convert = function (buffer, dataIndex) + { + // DICOM parser + var dicomParser = new dwv.dicom.DicomParser(); + dicomParser.setDefaultCharacterSet(defaultCharacterSet); + // parse the buffer + dicomParser.parse(buffer); + + var pixelBuffer = dicomParser.getRawDicomElements().x7FE00010.value; + var syntax = dwv.dicom.cleanString(dicomParser.getRawDicomElements().x00020010.value[0]); + var algoName = dwv.dicom.getSyntaxDecompressionName(syntax); + var needDecompression = (algoName !== null); + + // worker callback + var onDecodedFirstFrame = function (/*event*/) { + // create the image + var imageFactory = new dwv.image.ImageFactory(); + var image = imageFactory.create( dicomParser.getDicomElements(), pixelBuffer ); + // create the view + var viewFactory = new dwv.image.ViewFactory(); + var view = viewFactory.create( dicomParser.getDicomElements(), image ); + // return + self.onload({"view": view, "info": dicomParser.getDicomElements().dumpToTable()}); + }; + + if ( needDecompression ) { + var bitsAllocated = dicomParser.getRawDicomElements().x00280100.value[0]; + var pixelRepresentation = dicomParser.getRawDicomElements().x00280103.value[0]; + var isSigned = (pixelRepresentation === 1); + var nFrames = pixelBuffer.length; + + if (!pixelDecoder){ + pixelDecoder = new dwv.image.PixelBufferDecoder(algoName); + } + + // loadend event + pixelDecoder.ondecodeend = function () { + self.onloadend(); + }; + + // send an onload event for mono frame + if ( nFrames === 1 ) { + pixelDecoder.ondecoded = function () { + self.onloadend(); + }; + } + + // decoder callback + var countDecodedFrames = 0; + var onDecodedFrame = function (frame) { + return function (event) { + // send progress + ++countDecodedFrames; + var ev = {'type': "load-progress", 'lengthComputable': true, + 'loaded': (countDecodedFrames * 100 / nFrames), 'total': 100}; + if ( typeof dataIndex !== "undefined") { + ev.index = dataIndex; + } + self.onprogress(ev); + // store data + pixelBuffer[frame] = event.data[0]; + // create image for first frame + if ( frame === 0 ) { + onDecodedFirstFrame(); + } + }; + }; + + // decompress synchronously the first frame to create the image + pixelDecoder.decode(pixelBuffer[0], + bitsAllocated, isSigned, onDecodedFrame(0), false); + + // decompress the possible other frames + if ( nFrames !== 1 ) { + // decode (asynchronously if possible) + for (var f = 1; f < nFrames; ++f) { + pixelDecoder.decode(pixelBuffer[f], + bitsAllocated, isSigned, onDecodedFrame(f)); + } + } + } + // no decompression + else { + // send progress + var evnodec = {'type': 'load-progress', 'lengthComputable': true, + 'loaded': 100, 'total': 100}; + if ( typeof dataIndex !== "undefined") { + evnodec.index = dataIndex; + } + self.onprogress(evnodec); + // create image + onDecodedFirstFrame(); + // send load events + self.onloadend(); + } + }; + + /** + * Abort a conversion. + */ + this.abort = function () { + if ( pixelDecoder ) { + pixelDecoder.abort(); + } + }; +}; + +/** + * Handle a load end event. + * @param {Object} event The load end event. + * Default does nothing. + */ +dwv.image.DicomBufferToView.prototype.onloadend = function (/*event*/) {}; +/** + * Handle a load event. + * @param {Object} event The load event. + * Default does nothing. + */ +dwv.image.DicomBufferToView.prototype.onload = function (/*event*/) {}; +/** + * Handle a load progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.image.DicomBufferToView.prototype.onprogress = function (/*event*/) {}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Create a simple array buffer from an ImageData buffer. + * @param {Object} imageData The ImageData taken from a context. + * @return {Array} The image buffer. + */ +dwv.image.imageDataToBuffer = function (imageData) { + // remove alpha + // TODO support passing the full image data + var dataLen = imageData.data.length; + var buffer = new Uint8Array( (dataLen / 4) * 3); + var j = 0; + for( var i = 0; i < dataLen; i+=4 ) { + buffer[j] = imageData.data[i]; + buffer[j+1] = imageData.data[i+1]; + buffer[j+2] = imageData.data[i+2]; + j+=3; + } + return buffer; +}; + +/** + * Get data from an input context imageData. + * @param {Number} width The width of the coresponding image. + * @param {Number} height The height of the coresponding image. + * @param {Number} sliceIndex The slice index of the imageData. + * @param {Object} imageBuffer The image buffer. + * @param {Number} numberOfFrames The final number of frames. + * @return {Object} The corresponding view. + */ +dwv.image.getDefaultView = function ( + width, height, sliceIndex, + imageBuffer, numberOfFrames, info) { + // image size + var imageSize = new dwv.image.Size(width, height); + // default spacing + // TODO: misleading... + var imageSpacing = new dwv.image.Spacing(1,1); + // default origin + var origin = new dwv.math.Point3D(0,0,sliceIndex); + // create image + var geometry = new dwv.image.Geometry(origin, imageSize, imageSpacing ); + var image = new dwv.image.Image( geometry, imageBuffer, numberOfFrames ); + image.setPhotometricInterpretation("RGB"); + // meta information + var meta = {}; + meta.BitsStored = 8; + image.setMeta(meta); + // overlay + image.setFirstOverlay( dwv.gui.info.createOverlaysForDom(info) ); + // view + var view = new dwv.image.View(image); + // defaut preset + view.setWindowLevelMinMax(); + // return + return view; +}; + +/** + * Get data from an input image using a canvas. + * @param {Object} image The DOM Image. + * @return {Mixed} The corresponding view and info. + */ +dwv.image.getViewFromDOMImage = function (image) +{ + // image size + var width = image.width; + var height = image.height; + + // draw the image in the canvas in order to get its data + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + // get the image data + var imageData = ctx.getImageData(0, 0, width, height); + + // image properties + var info = []; + if ( typeof image.origin === "string" ) { + info.push({ "name": "origin", "value": image.origin }); + } else { + info.push({ "name": "fileName", "value": image.origin.name }); + info.push({ "name": "fileType", "value": image.origin.type }); + info.push({ "name": "fileLastModifiedDate", "value": image.origin.lastModifiedDate }); + } + info.push({ "name": "imageWidth", "value": width }); + info.push({ "name": "imageHeight", "value": height }); + + // create view + var sliceIndex = image.index ? image.index : 0; + var imageBuffer = dwv.image.imageDataToBuffer(imageData); + var view = dwv.image.getDefaultView( + width, height, sliceIndex, [imageBuffer], 1, info); + + // return + return {"view": view, "info": info}; +}; + +/** + * Get data from an input image using a canvas. + * @param {Object} video The DOM Video. + * @param {Object} callback The function to call once the data is loaded. + * @param {Object} cbprogress The function to call to report progress. + * @param {Object} cbonloadend The function to call to report load end. + * @param {Number} dataindex The data index. + */ +dwv.image.getViewFromDOMVideo = function (video, callback, cbprogress, cbonloadend, dataIndex) +{ + // video size + var width = video.videoWidth; + var height = video.videoHeight; + + // default frame rate... + var frameRate = 30; + // number of frames + var numberOfFrames = Math.floor(video.duration * frameRate); + + // video properties + var info = []; + if( video.file ) + { + info.push({ "name": "fileName", "value": video.file.name }); + info.push({ "name": "fileType", "value": video.file.type }); + info.push({ "name": "fileLastModifiedDate", "value": video.file.lastModifiedDate }); + } + info.push({ "name": "imageWidth", "value": width }); + info.push({ "name": "imageHeight", "value": height }); + info.push({ "name": "numberOfFrames", "value": numberOfFrames }); + + // draw the image in the canvas in order to get its data + var canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext('2d'); + + // using seeked to loop through all video frames + video.addEventListener('seeked', onseeked, false); + + // current frame index + var frameIndex = 0; + // video view + var view = null; + + // draw the context and store it as a frame + function storeFrame() { + // send progress + var evprog = {'type': 'load-progress', 'lengthComputable': true, + 'loaded': frameIndex, 'total': numberOfFrames}; + if (typeof dataIndex !== "undefined") { + evprog.index = dataIndex; + } + cbprogress(evprog); + // draw image + ctx.drawImage(video, 0, 0); + // context to image buffer + var imgBuffer = dwv.image.imageDataToBuffer( + ctx.getImageData(0, 0, width, height) ); + if (frameIndex === 0) { + // create view + view = dwv.image.getDefaultView( + width, height, 1, [imgBuffer], numberOfFrames, info); + // call callback + callback( {"view": view, "info": info } ); + } else { + view.appendFrameBuffer(imgBuffer); + } + } + + // handle seeked event + function onseeked(/*event*/) { + // store + storeFrame(); + // increment index + ++frameIndex; + // set the next time + // (not using currentTime, it seems to get offseted) + var nextTime = frameIndex / frameRate; + if (nextTime <= this.duration) { + this.currentTime = nextTime; + } else { + cbonloadend(); + // stop listening + video.removeEventListener('seeked', onseeked); + } + } + + // trigger the first seeked + video.currentTime = 0; +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; +/** @namespace */ +dwv.image.filter = dwv.image.filter || {}; + +/** + * Threshold an image between an input minimum and maximum. + * @constructor + */ +dwv.image.filter.Threshold = function() +{ + /** + * Threshold minimum. + * @private + * @type Number + */ + var min = 0; + /** + * Threshold maximum. + * @private + * @type Number + */ + var max = 0; + + /** + * Get the threshold minimum. + * @return {Number} The threshold minimum. + */ + this.getMin = function() { return min; }; + /** + * Set the threshold minimum. + * @param {Number} val The threshold minimum. + */ + this.setMin = function(val) { min = val; }; + /** + * Get the threshold maximum. + * @return {Number} The threshold maximum. + */ + this.getMax = function() { return max; }; + /** + * Set the threshold maximum. + * @param {Number} val The threshold maximum. + */ + this.setMax = function(val) { max = val; }; + /** + * Get the name of the filter. + * @return {String} The name of the filter. + */ + this.getName = function() { return "Threshold"; }; + + /** + * Original image. + * @private + * @type Object + */ + var originalImage = null; + /** + * Set the original image. + * @param {Object} image The original image. + */ + this.setOriginalImage = function (image) { originalImage = image; }; + /** + * Get the original image. + * @return {Object} image The original image. + */ + this.getOriginalImage = function () { return originalImage; }; +}; + +/** + * Transform the main image using this filter. + * @return {Object} The transformed image. + */ +dwv.image.filter.Threshold.prototype.update = function () +{ + var image = this.getOriginalImage(); + var imageMin = image.getDataRange().min; + var self = this; + var threshFunction = function (value) { + if ( value < self.getMin() || value > self.getMax() ) { + return imageMin; + } + else { + return value; + } + }; + return image.transform( threshFunction ); +}; + +/** + * Sharpen an image using a sharpen convolution matrix. + * @constructor + */ +dwv.image.filter.Sharpen = function() +{ + /** + * Get the name of the filter. + * @return {String} The name of the filter. + */ + this.getName = function() { return "Sharpen"; }; + /** + * Original image. + * @private + * @type Object + */ + var originalImage = null; + /** + * Set the original image. + * @param {Object} image The original image. + */ + this.setOriginalImage = function (image) { originalImage = image; }; + /** + * Get the original image. + * @return {Object} image The original image. + */ + this.getOriginalImage = function () { return originalImage; }; +}; + +/** + * Transform the main image using this filter. + * @return {Object} The transformed image. + */ +dwv.image.filter.Sharpen.prototype.update = function() +{ + var image = this.getOriginalImage(); + + return image.convolute2D( + [ 0, -1, 0, + -1, 5, -1, + 0, -1, 0 ] ); +}; + +/** + * Apply a Sobel filter to an image. + * @constructor + */ +dwv.image.filter.Sobel = function() +{ + /** + * Get the name of the filter. + * @return {String} The name of the filter. + */ + this.getName = function() { return "Sobel"; }; + /** + * Original image. + * @private + * @type Object + */ + var originalImage = null; + /** + * Set the original image. + * @param {Object} image The original image. + */ + this.setOriginalImage = function (image) { originalImage = image; }; + /** + * Get the original image. + * @return {Object} image The original image. + */ + this.getOriginalImage = function () { return originalImage; }; +}; + +/** + * Transform the main image using this filter. + * @return {Object} The transformed image. + */ +dwv.image.filter.Sobel.prototype.update = function() +{ + var image = this.getOriginalImage(); + + var gradX = image.convolute2D( + [ 1, 0, -1, + 2, 0, -2, + 1, 0, -1 ] ); + + var gradY = image.convolute2D( + [ 1, 2, 1, + 0, 0, 0, + -1, -2, -1 ] ); + + return gradX.compose( gradY, function (x,y) { return Math.sqrt(x*x+y*y); } ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * 2D/3D Size class. + * @constructor + * @param {Number} numberOfColumns The number of columns. + * @param {Number} numberOfRows The number of rows. + * @param {Number} numberOfSlices The number of slices. +*/ +dwv.image.Size = function ( numberOfColumns, numberOfRows, numberOfSlices ) +{ + /** + * Get the number of columns. + * @return {Number} The number of columns. + */ + this.getNumberOfColumns = function () { return numberOfColumns; }; + /** + * Get the number of rows. + * @return {Number} The number of rows. + */ + this.getNumberOfRows = function () { return numberOfRows; }; + /** + * Get the number of slices. + * @return {Number} The number of slices. + */ + this.getNumberOfSlices = function () { return (numberOfSlices || 1.0); }; +}; + +/** + * Get the size of a slice. + * @return {Number} The size of a slice. + */ +dwv.image.Size.prototype.getSliceSize = function () { + return this.getNumberOfColumns() * this.getNumberOfRows(); +}; + +/** + * Get the total size. + * @return {Number} The total size. + */ +dwv.image.Size.prototype.getTotalSize = function () { + return this.getSliceSize() * this.getNumberOfSlices(); +}; + +/** + * Check for equality. + * @param {Size} rhs The object to compare to. + * @return {Boolean} True if both objects are equal. + */ +dwv.image.Size.prototype.equals = function (rhs) { + return rhs !== null && + this.getNumberOfColumns() === rhs.getNumberOfColumns() && + this.getNumberOfRows() === rhs.getNumberOfRows() && + this.getNumberOfSlices() === rhs.getNumberOfSlices(); +}; + +/** + * Check that coordinates are within bounds. + * @param {Number} i The column coordinate. + * @param {Number} j The row coordinate. + * @param {Number} k The slice coordinate. + * @return {Boolean} True if the given coordinates are within bounds. + */ +dwv.image.Size.prototype.isInBounds = function ( i, j, k ) { + if( i < 0 || i > this.getNumberOfColumns() - 1 || + j < 0 || j > this.getNumberOfRows() - 1 || + k < 0 || k > this.getNumberOfSlices() - 1 ) { + return false; + } + return true; +}; + +/** + * Get a string representation of the Vector3D. + * @return {String} The vector as a string. + */ +dwv.image.Size.prototype.toString = function () { + return "(" + this.getNumberOfColumns() + + ", " + this.getNumberOfRows() + + ", " + this.getNumberOfSlices() + ")"; +}; + +/** + * 2D/3D Spacing class. + * @constructor + * @param {Number} columnSpacing The column spacing. + * @param {Number} rowSpacing The row spacing. + * @param {Number} sliceSpacing The slice spacing. + */ +dwv.image.Spacing = function ( columnSpacing, rowSpacing, sliceSpacing ) +{ + /** + * Get the column spacing. + * @return {Number} The column spacing. + */ + this.getColumnSpacing = function () { return columnSpacing; }; + /** + * Get the row spacing. + * @return {Number} The row spacing. + */ + this.getRowSpacing = function () { return rowSpacing; }; + /** + * Get the slice spacing. + * @return {Number} The slice spacing. + */ + this.getSliceSpacing = function () { return (sliceSpacing || 1.0); }; +}; + +/** + * Check for equality. + * @param {Spacing} rhs The object to compare to. + * @return {Boolean} True if both objects are equal. + */ +dwv.image.Spacing.prototype.equals = function (rhs) { + return rhs !== null && + this.getColumnSpacing() === rhs.getColumnSpacing() && + this.getRowSpacing() === rhs.getRowSpacing() && + this.getSliceSpacing() === rhs.getSliceSpacing(); +}; + +/** + * Get a string representation of the Vector3D. + * @return {String} The vector as a string. + */ +dwv.image.Spacing.prototype.toString = function () { + return "(" + this.getColumnSpacing() + + ", " + this.getRowSpacing() + + ", " + this.getSliceSpacing() + ")"; +}; + + +/** + * 2D/3D Geometry class. + * @constructor + * @param {Object} origin The object origin (a 3D point). + * @param {Object} size The object size. + * @param {Object} spacing The object spacing. + * @param {Object} orientation The object orientation (3*3 matrix, default to 3*3 identity). + */ +dwv.image.Geometry = function ( origin, size, spacing, orientation ) +{ + // check input origin + if( typeof origin === 'undefined' ) { + origin = new dwv.math.Point3D(0,0,0); + } + var origins = [origin]; + // check input orientation + if( typeof orientation === 'undefined' ) { + orientation = new dwv.math.getIdentityMat33(); + } + + /** + * Get the object first origin. + * @return {Object} The object first origin. + */ + this.getOrigin = function () { return origin; }; + /** + * Get the object origins. + * @return {Array} The object origins. + */ + this.getOrigins = function () { return origins; }; + /** + * Get the object size. + * @return {Object} The object size. + */ + this.getSize = function () { return size; }; + /** + * Get the object spacing. + * @return {Object} The object spacing. + */ + this.getSpacing = function () { return spacing; }; + /** + * Get the object orientation. + * @return {Object} The object orientation. + */ + this.getOrientation = function () { return orientation; }; + + /** + * Get the slice position of a point in the current slice layout. + * @param {Object} point The point to evaluate. + */ + this.getSliceIndex = function (point) + { + // cannot use this.worldToIndex(point).getK() since + // we cannot guaranty consecutive slices... + + // find the closest index + var closestSliceIndex = 0; + var minDist = point.getDistance(origins[0]); + var dist = 0; + for( var i = 0; i < origins.length; ++i ) + { + dist = point.getDistance(origins[i]); + if( dist < minDist ) + { + minDist = dist; + closestSliceIndex = i; + } + } + // we have the closest point, are we before or after + var normal = new dwv.math.Vector3D( + orientation.get(2,0), orientation.get(2,1), orientation.get(2,2)); + var dotProd = normal.dotProduct( point.minus(origins[closestSliceIndex]) ); + var sliceIndex = ( dotProd > 0 ) ? closestSliceIndex + 1 : closestSliceIndex; + return sliceIndex; + }; + + /** + * Append an origin to the geometry. + * @param {Object} origin The origin to append. + * @param {Number} index The index at which to append. + */ + this.appendOrigin = function (origin, index) + { + // add in origin array + origins.splice(index, 0, origin); + // increment slice number + size = new dwv.image.Size( + size.getNumberOfColumns(), + size.getNumberOfRows(), + size.getNumberOfSlices() + 1); + }; + +}; + +/** + * Check for equality. + * @param {Geometry} rhs The object to compare to. + * @return {Boolean} True if both objects are equal. + */ +dwv.image.Geometry.prototype.equals = function (rhs) { + return rhs !== null && + this.getOrigin() === rhs.getOrigin() && + this.getSize() === rhs.getSize() && + this.getSpacing() === rhs.getSpacing(); +}; + +/** + * Convert an index to an offset in memory. + * @param {Object} index The index to convert. + */ +dwv.image.Geometry.prototype.indexToOffset = function (index) { + var size = this.getSize(); + return index.getI() + + index.getJ() * size.getNumberOfColumns() + + index.getK() * size.getSliceSize(); +}; + +/** + * Convert an index into world coordinates. + * @param {Object} index The index to convert. + */ +dwv.image.Geometry.prototype.indexToWorld = function (index) { + var origin = this.getOrigin(); + var spacing = this.getSpacing(); + return new dwv.math.Point3D( + origin.getX() + index.getI() * spacing.getColumnSpacing(), + origin.getY() + index.getJ() * spacing.getRowSpacing(), + origin.getZ() + index.getK() * spacing.getSliceSpacing() ); +}; + +/** + * Convert world coordinates into an index. + * @param {Object} THe point to convert. + */ +dwv.image.Geometry.prototype.worldToIndex = function (point) { + var origin = this.getOrigin(); + var spacing = this.getSpacing(); + return new dwv.math.Point3D( + point.getX() / spacing.getColumnSpacing() - origin.getX(), + point.getY() / spacing.getRowSpacing() - origin.getY(), + point.getZ() / spacing.getSliceSpacing() - origin.getZ() ); +}; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.image = dwv.image || {}; + +/** + * Rescale Slope and Intercept + * @constructor + * @param slope + * @param intercept + */ +dwv.image.RescaleSlopeAndIntercept = function (slope, intercept) +{ + /*// Check the rescale slope. + if(typeof(slope) === 'undefined') { + slope = 1; + } + // Check the rescale intercept. + if(typeof(intercept) === 'undefined') { + intercept = 0; + }*/ + + /** + * Get the slope of the RSI. + * @return {Number} The slope of the RSI. + */ + this.getSlope = function () + { + return slope; + }; + /** + * Get the intercept of the RSI. + * @return {Number} The intercept of the RSI. + */ + this.getIntercept = function () + { + return intercept; + }; + /** + * Apply the RSI on an input value. + * @return {Number} The value to rescale. + */ + this.apply = function (value) + { + return value * slope + intercept; + }; +}; + +/** + * Check for RSI equality. + * @param {Object} rhs The other RSI to compare to. + * @return {Boolean} True if both RSI are equal. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.equals = function (rhs) { + return rhs !== null && + this.getSlope() === rhs.getSlope() && + this.getIntercept() === rhs.getIntercept(); +}; + +/** + * Get a string representation of the RSI. + * @return {String} The RSI as a string. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.toString = function () { + return (this.getSlope() + ", " + this.getIntercept()); +}; + +/** + * Is this RSI an ID RSI. + * @return {Boolean} True if the RSI has a slope of 1 and no intercept. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.isID = function () { + return (this.getSlope() === 1 && this.getIntercept() === 0); +}; + +/** + * Image class. + * Usable once created, optional are: + * - rescale slope and intercept (default 1:0), + * - photometric interpretation (default MONOCHROME2), + * - planar configuration (default RGBRGB...). + * @constructor + * @param {Object} geometry The geometry of the image. + * @param {Array} buffer The image data as an array of frame buffers. + * @param {Number} numberOfFrames The number of frames (optional, can be used + to anticipate the final number after appends). + */ +dwv.image.Image = function(geometry, buffer, numberOfFrames) +{ + // use buffer length in not specified + if (typeof numberOfFrames === "undefined") { + numberOfFrames = buffer.length; + } + + /** + * Get the number of frames. + * @returns {Number} The number of frames. + */ + this.getNumberOfFrames = function () { + return numberOfFrames; + }; + + /** + * Rescale slope and intercept. + * @private + * @type Number + */ + var rsis = []; + // initialise RSIs + for ( var s = 0, nslices = geometry.getSize().getNumberOfSlices(); s < nslices; ++s ) { + rsis.push( new dwv.image.RescaleSlopeAndIntercept( 1, 0 ) ); + } + /** + * Flag to know if the RSIs are all identity (1,0). + * @private + * @type Boolean + */ + var isIdentityRSI = true; + /** + * Flag to know if the RSIs are all equals. + * @private + * @type Boolean + */ + var isConstantRSI = true; + /** + * Photometric interpretation (MONOCHROME, RGB...). + * @private + * @type String + */ + var photometricInterpretation = "MONOCHROME2"; + /** + * Planar configuration for RGB data (0:RGBRGBRGBRGB... or 1:RRR...GGG...BBB...). + * @private + * @type Number + */ + var planarConfiguration = 0; + /** + * Number of components. + * @private + * @type Number + */ + var numberOfComponents = buffer[0].length / ( + geometry.getSize().getTotalSize() ); + /** + * Meta information. + * @private + * @type Object + */ + var meta = {}; + + /** + * Data range. + * @private + * @type Object + */ + var dataRange = null; + /** + * Rescaled data range. + * @private + * @type Object + */ + var rescaledDataRange = null; + /** + * Histogram. + * @private + * @type Array + */ + var histogram = null; + + /** + * Overlay. + * @private + * @type Array + */ + var overlays = []; + + /** + * Set the first overlay. + * @param {Array} over The first overlay. + */ + this.setFirstOverlay = function (over) { overlays[0] = over; }; + + /** + * Get the overlays. + * @return {Array} The overlays array. + */ + this.getOverlays = function () { return overlays; }; + + /** + * Get the geometry of the image. + * @return {Object} The size of the image. + */ + this.getGeometry = function() { return geometry; }; + + /** + * Get the data buffer of the image. + * @todo dangerous... + * @return {Array} The data buffer of the image. + */ + this.getBuffer = function() { return buffer; }; + /** + * Get the data buffer of the image. + * @todo dangerous... + * @return {Array} The data buffer of the frame. + */ + this.getFrame = function (frame) { return buffer[frame]; }; + + /** + * Get the rescale slope and intercept. + * @param {Number} k The slice index. + * @return {Object} The rescale slope and intercept. + */ + this.getRescaleSlopeAndIntercept = function(k) { return rsis[k]; }; + /** + * Set the rescale slope and intercept. + * @param {Array} inRsi The input rescale slope and intercept. + * @param {Number} k The slice index (optional). + */ + this.setRescaleSlopeAndIntercept = function(inRsi, k) { + if ( typeof k === 'undefined' ) { + k = 0; + } + rsis[k] = inRsi; + + // update RSI flags + isIdentityRSI = true; + isConstantRSI = true; + for ( var s = 0, lens = rsis.length; s < lens; ++s ) { + if (!rsis[s].isID()) { + isIdentityRSI = false; + } + if (s > 0 && !rsis[s].equals(rsis[s-1])) { + isConstantRSI = false; + } + } + }; + /** + * Are all the RSIs identity (1,0). + * @return {Boolean} True if they are. + */ + this.isIdentityRSI = function () { return isIdentityRSI; }; + /** + * Are all the RSIs equal. + * @return {Boolean} True if they are. + */ + this.isConstantRSI = function () { return isConstantRSI; }; + /** + * Get the photometricInterpretation of the image. + * @return {String} The photometricInterpretation of the image. + */ + this.getPhotometricInterpretation = function() { return photometricInterpretation; }; + /** + * Set the photometricInterpretation of the image. + * @pqrqm {String} interp The photometricInterpretation of the image. + */ + this.setPhotometricInterpretation = function(interp) { photometricInterpretation = interp; }; + /** + * Get the planarConfiguration of the image. + * @return {Number} The planarConfiguration of the image. + */ + this.getPlanarConfiguration = function() { return planarConfiguration; }; + /** + * Set the planarConfiguration of the image. + * @param {Number} config The planarConfiguration of the image. + */ + this.setPlanarConfiguration = function(config) { planarConfiguration = config; }; + /** + * Get the numberOfComponents of the image. + * @return {Number} The numberOfComponents of the image. + */ + this.getNumberOfComponents = function() { return numberOfComponents; }; + + /** + * Get the meta information of the image. + * @return {Object} The meta information of the image. + */ + this.getMeta = function() { return meta; }; + /** + * Set the meta information of the image. + * @param {Object} rhs The meta information of the image. + */ + this.setMeta = function(rhs) { meta = rhs; }; + + /** + * Get value at offset. Warning: No size check... + * @param {Number} offset The desired offset. + * @param {Number} frame The desired frame. + * @return {Number} The value at offset. + */ + this.getValueAtOffset = function (offset, frame) { + return buffer[frame][offset]; + }; + + /** + * Clone the image. + * @return {Image} A clone of this image. + */ + this.clone = function() + { + // clone the image buffer + var clonedBuffer = []; + for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { + clonedBuffer[f] = buffer[f].slice(0); + } + // create the image copy + var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer); + // copy the RSIs + var nslices = this.getGeometry().getSize().getNumberOfSlices(); + for ( var k = 0; k < nslices; ++k ) { + copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept(k), k); + } + // copy extras + copy.setPhotometricInterpretation(this.getPhotometricInterpretation()); + copy.setPlanarConfiguration(this.getPlanarConfiguration()); + copy.setMeta(this.getMeta()); + // return + return copy; + }; + + /** + * Append a slice to the image. + * @param {Image} The slice to append. + * @return {Number} The number of the inserted slice. + */ + this.appendSlice = function (rhs, frame) + { + // check input + if( rhs === null ) { + throw new Error("Cannot append null slice"); + } + var rhsSize = rhs.getGeometry().getSize(); + var size = geometry.getSize(); + if( rhsSize.getNumberOfSlices() !== 1 ) { + throw new Error("Cannot append more than one slice"); + } + if( size.getNumberOfColumns() !== rhsSize.getNumberOfColumns() ) { + throw new Error("Cannot append a slice with different number of columns"); + } + if( size.getNumberOfRows() !== rhsSize.getNumberOfRows() ) { + throw new Error("Cannot append a slice with different number of rows"); + } + if( photometricInterpretation !== rhs.getPhotometricInterpretation() ) { + throw new Error("Cannot append a slice with different photometric interpretation"); + } + // all meta should be equal + for( var key in meta ) { + if( meta[key] !== rhs.getMeta()[key] ) { + throw new Error("Cannot append a slice with different "+key); + } + } + + var f = (typeof frame === "undefined") ? 0 : frame; + + // calculate slice size + var mul = 1; + if( photometricInterpretation === "RGB" || photometricInterpretation === "YBR_FULL_422") { + mul = 3; + } + var sliceSize = mul * size.getSliceSize(); + + // create the new buffer + var newBuffer = dwv.dicom.getTypedArray( + buffer[f].BYTES_PER_ELEMENT * 8, + meta.IsSigned ? 1 : 0, + sliceSize * (size.getNumberOfSlices() + 1) ); + + // append slice at new position + var newSliceNb = geometry.getSliceIndex( rhs.getGeometry().getOrigin() ); + if( newSliceNb === 0 ) + { + newBuffer.set(rhs.getFrame(f)); + newBuffer.set(buffer[f], sliceSize); + } + else if( newSliceNb === size.getNumberOfSlices() ) + { + newBuffer.set(buffer[f]); + newBuffer.set(rhs.getFrame(f), size.getNumberOfSlices() * sliceSize); + } + else + { + var offset = newSliceNb * sliceSize; + newBuffer.set(buffer[f].subarray(0, offset - 1)); + newBuffer.set(rhs.getFrame(f), offset); + newBuffer.set(buffer[f].subarray(offset), offset + sliceSize); + } + + // update geometry + geometry.appendOrigin( rhs.getGeometry().getOrigin(), newSliceNb ); + // update rsi + rsis.splice(newSliceNb, 0, rhs.getRescaleSlopeAndIntercept(0)); + + // copy to class variables + buffer[f] = newBuffer; + + // insert overlay information of the slice to the image + overlays.splice(newSliceNb, 0, rhs.getOverlays()[0]); + + // return the appended slice number + return newSliceNb; + }; + + /** + * Append a frame buffer to the image. + * @param {Object} frameBuffer The frame buffer to append. + */ + this.appendFrameBuffer = function (frameBuffer) + { + buffer.push(frameBuffer); + }; + + /** + * Get the data range. + * @return {Object} The data range. + */ + this.getDataRange = function() { + if( !dataRange ) { + dataRange = this.calculateDataRange(); + } + return dataRange; + }; + + /** + * Get the rescaled data range. + * @return {Object} The rescaled data range. + */ + this.getRescaledDataRange = function() { + if( !rescaledDataRange ) { + rescaledDataRange = this.calculateRescaledDataRange(); + } + return rescaledDataRange; + }; + + /** + * Get the histogram. + * @return {Array} The histogram. + */ + this.getHistogram = function() { + if( !histogram ) { + var res = this.calculateHistogram(); + dataRange = res.dataRange; + rescaledDataRange = res.rescaledDataRange; + histogram = res.histogram; + } + return histogram; + }; +}; + +/** + * Get the value of the image at a specific coordinate. + * @param {Number} i The X index. + * @param {Number} j The Y index. + * @param {Number} k The Z index. + * @param {Number} f The frame number. + * @return {Number} The value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getValue = function( i, j, k, f ) +{ + var frame = (f || 0); + var index = new dwv.math.Index3D(i,j,k); + return this.getValueAtOffset( this.getGeometry().indexToOffset(index), frame ); +}; + +/** + * Get the rescaled value of the image at a specific coordinate. + * @param {Number} i The X index. + * @param {Number} j The Y index. + * @param {Number} k The Z index. + * @param {Number} f The frame number. + * @return {Number} The rescaled value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getRescaledValue = function( i, j, k, f ) +{ + var frame = (f || 0); + var val = this.getValue(i,j,k,frame); + if (!this.isIdentityRSI()) { + val = this.getRescaleSlopeAndIntercept(k).apply(val); + } + return val; +}; + +/** + * Calculate the data range of the image. + * WARNING: for speed reasons, only calculated on the first frame... + * @return {Object} The range {min, max}. + */ +dwv.image.Image.prototype.calculateDataRange = function () +{ + var size = this.getGeometry().getSize().getTotalSize(); + var nFrames = 1; //this.getNumberOfFrames(); + var min = this.getValueAtOffset(0,0); + var max = min; + var value = 0; + for ( var f = 0; f < nFrames; ++f ) { + for ( var i = 0; i < size; ++i ) { + value = this.getValueAtOffset(i,f); + if( value > max ) { max = value; } + if( value < min ) { min = value; } + } + } + // return + return { "min": min, "max": max }; +}; + +/** + * Calculate the rescaled data range of the image. + * WARNING: for speed reasons, only calculated on the first frame... + * @return {Object} The range {min, max}. + */ +dwv.image.Image.prototype.calculateRescaledDataRange = function () +{ + if (this.isIdentityRSI()) { + return this.getDataRange(); + } + else if (this.isConstantRSI()) { + var range = this.getDataRange(); + var resmin = this.getRescaleSlopeAndIntercept(0).apply(range.min); + var resmax = this.getRescaleSlopeAndIntercept(0).apply(range.max); + return { + "min": ((resmin < resmax) ? resmin : resmax), + "max": ((resmin > resmax) ? resmin : resmax) + }; + } + else { + var size = this.getGeometry().getSize(); + var nFrames = 1; //this.getNumberOfFrames(); + var rmin = this.getRescaledValue(0,0,0); + var rmax = rmin; + var rvalue = 0; + for ( var f = 0, nframes = nFrames; f < nframes; ++f ) { + for ( var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k ) { + for ( var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j ) { + for ( var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i ) { + rvalue = this.getRescaledValue(i,j,k,f); + if( rvalue > rmax ) { rmax = rvalue; } + if( rvalue < rmin ) { rmin = rvalue; } + } + } + } + } + // return + return { "min": rmin, "max": rmax }; + } +}; + +/** + * Calculate the histogram of the image. + * @return {Object} The histogram, data range and rescaled data range. + */ +dwv.image.Image.prototype.calculateHistogram = function () +{ + var size = this.getGeometry().getSize(); + var histo = []; + var min = this.getValue(0,0,0); + var max = min; + var value = 0; + var rmin = this.getRescaledValue(0,0,0); + var rmax = rmin; + var rvalue = 0; + for ( var f = 0, nframes = this.getNumberOfFrames(); f < nframes; ++f ) { + for ( var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k ) { + for ( var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j ) { + for ( var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i ) { + value = this.getValue(i,j,k,f); + if( value > max ) { max = value; } + if( value < min ) { min = value; } + rvalue = this.getRescaleSlopeAndIntercept(k).apply(value); + if( rvalue > rmax ) { rmax = rvalue; } + if( rvalue < rmin ) { rmin = rvalue; } + histo[rvalue] = ( histo[rvalue] || 0 ) + 1; + } + } + } + } + // set data range + var dataRange = { "min": min, "max": max }; + var rescaledDataRange = { "min": rmin, "max": rmax }; + // generate data for plotting + var histogram = []; + for ( var b = rmin; b <= rmax; ++b ) { + histogram.push([b, ( histo[b] || 0 ) ]); + } + // return + return { 'dataRange': dataRange, 'rescaledDataRange': rescaledDataRange, + 'histogram': histogram }; +}; + +/** + * Convolute the image with a given 2D kernel. + * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix. + * @return {Image} The convoluted image. + * Note: Uses the raw buffer values. + */ +dwv.image.Image.prototype.convolute2D = function(weights) +{ + if(weights.length !== 9) { + throw new Error("The convolution matrix does not have a length of 9; it has "+weights.length); + } + + var newImage = this.clone(); + var newBuffer = newImage.getBuffer(); + + var imgSize = this.getGeometry().getSize(); + var ncols = imgSize.getNumberOfColumns(); + var nrows = imgSize.getNumberOfRows(); + var nslices = imgSize.getNumberOfSlices(); + var nframes = this.getNumberOfFrames(); + var ncomp = this.getNumberOfComponents(); + + // adapt to number of component and planar configuration + var factor = 1; + var componentOffset = 1; + var frameOffset = imgSize.getTotalSize(); + if( ncomp === 3 ) + { + frameOffset *= 3; + if( this.getPlanarConfiguration() === 0 ) + { + factor = 3; + } + else + { + componentOffset = imgSize.getTotalSize(); + } + } + + // allow special indent for matrices + /*jshint indent:false */ + + // default weight offset matrix + var wOff = []; + wOff[0] = (-ncols-1) * factor; wOff[1] = (-ncols) * factor; wOff[2] = (-ncols+1) * factor; + wOff[3] = -factor; wOff[4] = 0; wOff[5] = 1 * factor; + wOff[6] = (ncols-1) * factor; wOff[7] = (ncols) * factor; wOff[8] = (ncols+1) * factor; + + // border weight offset matrices + // borders are extended (see http://en.wikipedia.org/wiki/Kernel_%28image_processing%29) + + // i=0, j=0 + var wOff00 = []; + wOff00[0] = wOff[4]; wOff00[1] = wOff[4]; wOff00[2] = wOff[5]; + wOff00[3] = wOff[4]; wOff00[4] = wOff[4]; wOff00[5] = wOff[5]; + wOff00[6] = wOff[7]; wOff00[7] = wOff[7]; wOff00[8] = wOff[8]; + // i=0, j=* + var wOff0x = []; + wOff0x[0] = wOff[1]; wOff0x[1] = wOff[1]; wOff0x[2] = wOff[2]; + wOff0x[3] = wOff[4]; wOff0x[4] = wOff[4]; wOff0x[5] = wOff[5]; + wOff0x[6] = wOff[7]; wOff0x[7] = wOff[7]; wOff0x[8] = wOff[8]; + // i=0, j=nrows + var wOff0n = []; + wOff0n[0] = wOff[1]; wOff0n[1] = wOff[1]; wOff0n[2] = wOff[2]; + wOff0n[3] = wOff[4]; wOff0n[4] = wOff[4]; wOff0n[5] = wOff[5]; + wOff0n[6] = wOff[4]; wOff0n[7] = wOff[4]; wOff0n[8] = wOff[5]; + + // i=*, j=0 + var wOffx0 = []; + wOffx0[0] = wOff[3]; wOffx0[1] = wOff[4]; wOffx0[2] = wOff[5]; + wOffx0[3] = wOff[3]; wOffx0[4] = wOff[4]; wOffx0[5] = wOff[5]; + wOffx0[6] = wOff[6]; wOffx0[7] = wOff[7]; wOffx0[8] = wOff[8]; + // i=*, j=* -> wOff + // i=*, j=nrows + var wOffxn = []; + wOffxn[0] = wOff[0]; wOffxn[1] = wOff[1]; wOffxn[2] = wOff[2]; + wOffxn[3] = wOff[3]; wOffxn[4] = wOff[4]; wOffxn[5] = wOff[5]; + wOffxn[6] = wOff[3]; wOffxn[7] = wOff[4]; wOffxn[8] = wOff[5]; + + // i=ncols, j=0 + var wOffn0 = []; + wOffn0[0] = wOff[3]; wOffn0[1] = wOff[4]; wOffn0[2] = wOff[4]; + wOffn0[3] = wOff[3]; wOffn0[4] = wOff[4]; wOffn0[5] = wOff[4]; + wOffn0[6] = wOff[6]; wOffn0[7] = wOff[7]; wOffn0[8] = wOff[7]; + // i=ncols, j=* + var wOffnx = []; + wOffnx[0] = wOff[0]; wOffnx[1] = wOff[1]; wOffnx[2] = wOff[1]; + wOffnx[3] = wOff[3]; wOffnx[4] = wOff[4]; wOffnx[5] = wOff[4]; + wOffnx[6] = wOff[6]; wOffnx[7] = wOff[7]; wOffnx[8] = wOff[7]; + // i=ncols, j=nrows + var wOffnn = []; + wOffnn[0] = wOff[0]; wOffnn[1] = wOff[1]; wOffnn[2] = wOff[1]; + wOffnn[3] = wOff[3]; wOffnn[4] = wOff[4]; wOffnn[5] = wOff[4]; + wOffnn[6] = wOff[3]; wOffnn[7] = wOff[4]; wOffnn[8] = wOff[4]; + + // restore indent for rest of method + /*jshint indent:4 */ + + // loop vars + var pixelOffset = 0; + var newValue = 0; + var wOffFinal = []; + // go through the destination image pixels + for (var f=0; f is signed + var pixelRepresentation = dicomElements.getFromKey("x00280103"); + meta.IsSigned = false; + if ( pixelRepresentation ) { + meta.IsSigned = (pixelRepresentation === 1); + } + image.setMeta(meta); + + // overlay + image.setFirstOverlay( dwv.gui.info.createOverlays(dicomElements) ); + + return image; +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; +/** @namespace */ +dwv.image.lut = dwv.image.lut || {}; + +/** + * Rescale LUT class. + * @constructor + * @param {Object} rsi The rescale slope and intercept. + * @param {Number} bitsStored The number of bits used to store the data. + */ +dwv.image.lut.Rescale = function (rsi, bitsStored) +{ + /** + * The internal array. + * @private + * @type Array + */ + var lut = null; + + /** + * Flag to know if the lut is ready or not. + * @private + * @type Boolean + */ + var isReady = false; + + /** + * The size of the LUT array. + * @private + * @type Number + */ + var length = Math.pow(2, bitsStored); + + /** + * Get the Rescale Slope and Intercept (RSI). + * @return {Object} The rescale slope and intercept. + */ + this.getRSI = function () { return rsi; }; + + /** + * Is the lut ready to use or not? If not, the user must + * call 'initialise'. + * @return {Boolean} True if the lut is ready to use. + */ + this.isReady = function () { return isReady; }; + + /** + * Initialise the LUT. + */ + this.initialise = function () + { + // check if already initialised + if (isReady) { + return; + } + // create lut and fill it + lut = new Float32Array(length); + for ( var i = 0; i < length; ++i ) { + lut[i] = rsi.apply(i); + } + // update ready flag + isReady = true; + }; + + /** + * Get the length of the LUT array. + * @return {Number} The length of the LUT array. + */ + this.getLength = function () { return length; }; + + /** + * Get the value of the LUT at the given offset. + * @return {Number} The value of the LUT at the given offset. + */ + this.getValue = function (offset) + { + return lut[ offset ]; + }; +}; + +/** + * Window LUT class. + * @constructor + * @param {Number} rescaleLut The associated rescale LUT. + * @param {Boolean} isSigned Flag to know if the data is signed or not. + */ +dwv.image.lut.Window = function (rescaleLut, isSigned) +{ + /** + * The internal array: Uint8ClampedArray clamps between 0 and 255. + * @private + * @type Array + */ + var lut = null; + + /** + * The window level. + * @private + * @type {Object} + */ + var windowLevel = null; + + /** + * Flag to know if the lut is ready or not. + * @private + * @type Boolean + */ + var isReady = false; + + /** + * Shift for signed data. + * @private + * @type Number + */ + var signedShift = 0; + + /** + * Get the window / level. + * @return {Object} The window / level. + */ + this.getWindowLevel = function () { return windowLevel; }; + /** + * Get the signed flag. + * @return {Boolean} The signed flag. + */ + this.isSigned = function () { return isSigned; }; + /** + * Get the rescale lut. + * @return {Object} The rescale lut. + */ + this.getRescaleLut = function () { return rescaleLut; }; + + /** + * Is the lut ready to use or not? If not, the user must + * call 'update'. + * @return {Boolean} True if the lut is ready to use. + */ + this.isReady = function () { return isReady; }; + + /** + * Set the window center and width. + * @param {Object} wl The window level. + */ + this.setWindowLevel = function (wl) + { + // store the window values + windowLevel = wl; + // possible signed shift + signedShift = 0; + windowLevel.setSignedOffset(0); + if ( isSigned ) { + var size = rescaleLut.getLength(); + signedShift = size / 2; + windowLevel.setSignedOffset(rescaleLut.getRSI().getSlope() * signedShift); + } + // update ready flag + isReady = false; + }; + + /** + * Update the lut if needed.. + */ + this.update = function () + { + // check if we need to update + if ( isReady ) { + return; + } + + // check rescale lut + if (!rescaleLut.isReady()) { + rescaleLut.initialise(); + } + // create window lut + var size = rescaleLut.getLength(); + if (!lut) { + // use clamped array (polyfilled in browser.js) + lut = new Uint8ClampedArray(size); + } + // by default WindowLevel returns a value in the [0,255] range + // this is ok with regular Arrays and ClampedArray. + for ( var i = 0; i < size; ++i ) + { + lut[i] = windowLevel.apply( rescaleLut.getValue(i) ); + } + + // update ready flag + isReady = true; + }; + + /** + * Get the length of the LUT array. + * @return {Number} The length of the LUT array. + */ + this.getLength = function () { return lut.length; }; + + /** + * Get the value of the LUT at the given offset. + * @return {Number} The value of the LUT at the given offset. + */ + this.getValue = function (offset) + { + return lut[ offset + signedShift ]; + }; +}; + +/** +* Lookup tables for image colour display. +*/ + +dwv.image.lut.range_max = 256; + +dwv.image.lut.buildLut = function(func) +{ + var lut = []; + for( var i=0; i= third && i < 2*third ) { + return dwv.image.lut.range_max-1; + } + return 0; +}; + +dwv.image.lut.maxThirdThird = function(i) +{ + if( i >= 2*dwv.image.lut.range_max/3 ) { + return dwv.image.lut.range_max-1; + } + return 0; +}; + +dwv.image.lut.toMaxFirstThird = function(i) +{ + var val = i * 3; + if( val > dwv.image.lut.range_max-1 ) { + return dwv.image.lut.range_max-1; + } + return val; +}; + +dwv.image.lut.toMaxSecondThird = function(i) +{ + var third = dwv.image.lut.range_max/3; + var val = 0; + if( i >= third ) { + val = (i-third) * 3; + if( val > dwv.image.lut.range_max-1 ) { + return dwv.image.lut.range_max-1; + } + } + return val; +}; + +dwv.image.lut.toMaxThirdThird = function(i) +{ + var third = dwv.image.lut.range_max/3; + var val = 0; + if( i >= 2*third ) { + val = (i-2*third) * 3; + if( val > dwv.image.lut.range_max-1 ) { + return dwv.image.lut.range_max-1; + } + } + return val; +}; + +dwv.image.lut.zero = function(/*i*/) +{ + return 0; +}; + +dwv.image.lut.id = function(i) +{ + return i; +}; + +dwv.image.lut.invId = function(i) +{ + return (dwv.image.lut.range_max-1)-i; +}; + +// plain +dwv.image.lut.plain = { + "red": dwv.image.lut.buildLut(dwv.image.lut.id), + "green": dwv.image.lut.buildLut(dwv.image.lut.id), + "blue": dwv.image.lut.buildLut(dwv.image.lut.id) +}; + +// inverse plain +dwv.image.lut.invPlain = { + "red": dwv.image.lut.buildLut(dwv.image.lut.invId), + "green": dwv.image.lut.buildLut(dwv.image.lut.invId), + "blue": dwv.image.lut.buildLut(dwv.image.lut.invId) +}; + +//rainbow +dwv.image.lut.rainbow = { + "blue": [0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 255, 247, 239, 231, 223, 215, 207, 199, 191, 183, 175, 167, 159, 151, 143, 135, 127, 119, 111, 103, 95, 87, 79, 71, 63, 55, 47, 39, 31, 23, 15, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "green": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 253, 251, 249, 247, 245, 243, 241, 239, 237, 235, 233, 231, 229, 227, 225, 223, 221, 219, 217, 215, 213, 211, 209, 207, 205, 203, 201, 199, 197, 195, 193, 192, 189, 186, 183, 180, 177, 174, 171, 168, 165, 162, 159, 156, 153, 150, 147, 144, 141, 138, 135, 132, 129, 126, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, 90, 87, 84, 81, 78, 75, 72, 69, 66, 63, 60, 57, 54, 51, 48, 45, 42, 39, 36, 33, 30, 27, 24, 21, 18, 15, 12, 9, 6, 3], + "red": [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 62, 60, 58, 56, 54, 52, 50, 48, 46, 44, 42, 40, 38, 36, 34, 32, 30, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] +}; + +// hot +dwv.image.lut.hot = { + "red": dwv.image.lut.buildLut(dwv.image.lut.toMaxFirstThird), + "green": dwv.image.lut.buildLut(dwv.image.lut.toMaxSecondThird), + "blue": dwv.image.lut.buildLut(dwv.image.lut.toMaxThirdThird) +}; + +// hot iron +dwv.image.lut.hot_iron = { + "red": [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "green": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 255], + "blue": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 255] +}; + +// pet +dwv.image.lut.pet = { + "red": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "green": [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 128, 126, 124, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, 94, 92, 90, 88, 86, 84, 82, 80, 78, 76, 74, 72, 70, 68, 66, 64, 63, 61, 59, 57, 55, 53, 51, 49, 47, 45, 43, 41, 39, 37, 35, 33, 31, 29, 27, 25, 23, 21, 19, 17, 15, 13, 11, 9, 7, 5, 3, 1, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248, 250, 252, 255], + "blue": [0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 107, 109, 111, 113, 115, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, 223, 225, 227, 229, 231, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 252, 248, 244, 240, 236, 232, 228, 224, 220, 216, 212, 208, 204, 200, 196, 192, 188, 184, 180, 176, 172, 168, 164, 160, 156, 152, 148, 144, 140, 136, 132, 128, 124, 120, 116, 112, 108, 104, 100, 96, 92, 88, 84, 80, 76, 72, 68, 64, 60, 56, 52, 48, 44, 40, 36, 32, 28, 24, 20, 16, 12, 8, 4, 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 129, 133, 137, 141, 145, 149, 153, 157, 161, 165, 170, 174, 178, 182, 186, 190, 194, 198, 202, 206, 210, 214, 218, 222, 226, 230, 234, 238, 242, 246, 250, 255] +}; + +// hot metal blue +dwv.image.lut.hot_metal_blue = { + "red": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 52, 55, 57, 59, 62, 64, 66, 69, 71, 74, 76, 78, 81, 83, 85, 88, 90, 93, 96, 99, 102, 105, 108, 111, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 166, 169, 172, 175, 178, 181, 184, 187, 190, 194, 198, 201, 205, 209, 213, 217, 221, 224, 228, 232, 236, 240, 244, 247, 251, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "green": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 9, 11, 13, 15, 17, 19, 21, 23, 24, 26, 28, 30, 32, 34, 36, 38, 40, 41, 43, 45, 47, 49, 51, 53, 55, 56, 58, 60, 62, 64, 66, 68, 70, 72, 73, 75, 77, 79, 81, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 105, 107, 109, 111, 113, 115, 117, 119, 120, 122, 124, 126, 128, 130, 132, 134, 136, 137, 139, 141, 143, 145, 147, 149, 151, 152, 154, 156, 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 201, 203, 205, 207, 209, 211, 213, 215, 216, 218, 220, 222, 224, 226, 228, 229, 231, 233, 235, 237, 239, 240, 242, 244, 246, 248, 250, 251, 253, 255], + "blue": [0, 2, 4, 6, 8, 10, 12, 14, 16, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 197, 194, 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 153, 150, 144, 138, 132, 126, 121, 115, 109, 103, 97, 91, 85, 79, 74, 68, 62, 56, 50, 47, 44, 41, 38, 35, 32, 29, 26, 24, 21, 18, 15, 12, 9, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59, 62, 65, 68, 71, 74, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 126, 129, 132, 135, 138, 141, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 206, 210, 213, 216, 219, 223, 226, 229, 232, 236, 239, 242, 245, 249, 252, 255] +}; + +// pet 20 step +dwv.image.lut.pet_20step = { + "red": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "green": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + "blue": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] +}; + +// test +dwv.image.lut.test = { + "red": dwv.image.lut.buildLut(dwv.image.lut.id), + "green": dwv.image.lut.buildLut(dwv.image.lut.zero), + "blue": dwv.image.lut.buildLut(dwv.image.lut.zero) +}; + +//red +/*dwv.image.lut.red = { + "red": dwv.image.lut.buildLut(dwv.image.lut.max), + "green": dwv.image.lut.buildLut(dwv.image.lut.id), + "blue": dwv.image.lut.buildLut(dwv.image.lut.id) +};*/ + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * WindowLevel class. + * References: + * - DICOM [Window Center and Window Width]{@link http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.11.html#sect_C.11.2.1.2} + * Pseudo-code: + * if (x <= c - 0.5 - (w-1)/2), then y = ymin + * else if (x > c - 0.5 + (w-1)/2), then y = ymax, + * else y = ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax - ymin) + ymin + */ +dwv.image.WindowLevel = function (center, width) +{ + // avoid zero width + if ( width === 0 ) { + //throw new Error("A window level with a width of zero is not possible."); + console.log("A window level with a width of zero is not possible."); + console.log("try width = 200"); + width = 200; + } + + /** + * Signed data offset. + * @private + * @type Number + */ + var signedOffset = 0; + /** + * Output value minimum. + * @private + * @type Number + */ + var ymin = 0; + /** + * Output value maximum. + * @private + * @type Number + */ + var ymax = 255; + + /** + * Input value minimum (calculated). + * @private + * @type Number + */ + var xmin = null; + /** + * Input value maximum (calculated). + * @private + * @type Number + */ + var xmax = null; + /** + * Window level equation slope (calculated). + * @private + * @type Number + */ + var slope = null; + /** + * Window level equation intercept (calculated). + * @private + * @type Number + */ + var inter = null; + + /** + * Initialise members. + */ + function init() { + var c = center + signedOffset; + // from the standard + xmin = c - 0.5 - ( (width-1) / 2 ); + xmax = c - 0.5 + ( (width-1) / 2 ); + // develop the equation: + // y = ( ( x - (c - 0.5) ) / (w-1) + 0.5 ) * (ymax - ymin) + ymin + // y = ( x / (w-1) ) * (ymax - ymin) + ( -(c - 0.5) / (w-1) + 0.5 ) * (ymax - ymin) + ymin + slope = (ymax - ymin) / (width-1); + inter = ( -(c - 0.5) / (width-1) + 0.5 ) * (ymax - ymin) + ymin; + } + + // call init + init(); + + /** + * Get the window center. + * @return {Number} The window center. + */ + this.getCenter = function () { return center; }; + /** + * Get the window width. + * @return {Number} The window width. + */ + this.getWidth = function () { return width; }; + + /** + * Set the output value range. + * @param {Number} min The output value minimum. + * @param {Number} max The output value maximum. + */ + this.setRange = function (min, max) { + ymin = parseInt( min, 10 ); + ymax = parseInt( max, 10 ) ; + // re-initialise + init(); + }; + /** + * Set the signed offset. + * @param {Number} The signed data offset, typically: slope * ( size / 2). + */ + this.setSignedOffset = function (offset) { + signedOffset = offset; + // re-initialise + init(); + }; + + /** + * Apply the window level on an input value. + * @param {Number} The value to rescale as an integer. + * @return {Number} The leveled value, in the + * [ymin, ymax] range (default [0,255]). + */ + this.apply = function (value) + { + if ( value <= xmin ) { + return ymin; + } else if ( value > xmax ) { + return ymax; + } else { + return parseInt( ((value * slope) + inter), 10); + } + }; + +}; + +/** + * Check for window level equality. + * @param {Object} rhs The other window level to compare to. + * @return {Boolean} True if both window level are equal. + */ +dwv.image.WindowLevel.prototype.equals = function (rhs) { + return rhs !== null && + this.getCenter() === rhs.getCenter() && + this.getWidth() === rhs.getWidth(); +}; + +/** + * Get a string representation of the window level. + * @return {String} The window level as a string. + */ +dwv.image.WindowLevel.prototype.toString = function () { + return (this.getCenter() + ", " + this.getWidth()); +}; + +/** + * View class. + * @constructor + * @param {Image} image The associated image. + * Need to set the window lookup table once created + * (either directly or with helper methods). + */ +dwv.image.View = function (image) +{ + /** + * Window lookup tables, indexed per Rescale Slope and Intercept (RSI). + * @private + * @type Window + */ + var windowLuts = {}; + + /** + * Window presets. + * Minmax will be filled at first use (see view.setWindowLevelPreset). + * @private + * @type Object + */ + var windowPresets = { "minmax": {"name": "minmax"} }; + + /** + * Current window preset name. + * @private + * @type String + */ + var currentPresetName = null; + + /** + * colour map. + * @private + * @type Object + */ + var colourMap = dwv.image.lut.plain; + /** + * Current position. + * @private + * @type Object + */ + var currentPosition = {"i":0,"j":0,"k":0}; + /** + * Current frame. Zero based. + * @private + * @type Number + */ + var currentFrame = null; + + /** + * Get the associated image. + * @return {Image} The associated image. + */ + this.getImage = function() { return image; }; + /** + * Set the associated image. + * @param {Image} inImage The associated image. + */ + this.setImage = function(inImage) { image = inImage; }; + + /** + * Get the window LUT of the image. + * Warning: can be undefined in no window/level was set. + * @param {Object} rsi Optional image rsi, will take the one of the current slice otherwise. + * @return {Window} The window LUT of the image. + */ + this.getCurrentWindowLut = function (rsi) { + var sliceNumber = this.getCurrentPosition().k; + // use current rsi if not provided + if ( typeof rsi === "undefined" ) { + rsi = image.getRescaleSlopeAndIntercept(sliceNumber); + } + // get the lut + var wlut = windowLuts[ rsi.toString() ]; + + // special case for 'perslice' presets + if (currentPresetName && + typeof windowPresets[currentPresetName] !== "undefined" && + typeof windowPresets[currentPresetName].perslice !== "undefined" && + windowPresets[currentPresetName].perslice === true ) { + // get the preset for this slice + var wl = windowPresets[currentPresetName].wl[sliceNumber]; + // apply it if different from previous + if (!wlut.getWindowLevel().equals(wl)) { + // previous values + var previousWidth = wlut.getWindowLevel().getWidth(); + var previousCenter = wlut.getWindowLevel().getCenter(); + // set slice window level + wlut.setWindowLevel(wl); + // fire event + if ( previousWidth !== wl.getWidth() ) { + this.fireEvent({"type": "wl-width-change", + "wc": wl.getCenter(), "ww": wl.getWidth(), + "skipGenerate": true}); + } + if ( previousCenter !== wl.getCenter() ) { + this.fireEvent({"type": "wl-center-change", + "wc": wl.getCenter(), "ww": wl.getWidth(), + "skipGenerate": true}); + } + } + } + + // update in case of wl change + // TODO: should not be run in a getter... + wlut.update(); + + // return + return wlut; + }; + /** + * Add the window LUT to the list. + * @param {Window} wlut The window LUT of the image. + */ + this.addWindowLut = function (wlut) + { + var rsi = wlut.getRescaleLut().getRSI(); + windowLuts[rsi.toString()] = wlut; + }; + + /** + * Get the window presets. + * @return {Object} The window presets. + */ + this.getWindowPresets = function () { + return windowPresets; + }; + + /** + * Get the window presets names. + * @return {Object} The list of window presets names. + */ + this.getWindowPresetsNames = function () { + return Object.keys(windowPresets); + }; + + /** + * Set the window presets. + * @param {Object} presets The window presets. + */ + this.setWindowPresets = function (presets) { + windowPresets = presets; + }; + + /** + * Set the default colour map. + * @param {Object} map The colour map. + */ + this.setDefaultColourMap = function (map) { + colourMap = map; + }; + + /** + * Add window presets to the existing ones. + * @param {Object} presets The window presets. + * @param {Number} k The slice the preset belong to. + */ + this.addWindowPresets = function (presets, k) { + var keys = Object.keys(presets); + var key = null; + for (var i = 0; i < keys.length; ++i) { + key = keys[i]; + if (typeof windowPresets[key] !== "undefined") { + if (typeof windowPresets[key].perslice !== "undefined" && + windowPresets[key].perslice === true) { + // use first new preset wl... + windowPresets[key].wl.splice(k, 0, presets[key].wl[0]); + } else { + windowPresets[key] = presets[key]; + } + } else { + // add new + windowPresets[key] = presets[key]; + } + } + }; + + /** + * Get the colour map of the image. + * @return {Object} The colour map of the image. + */ + this.getColourMap = function() { return colourMap; }; + /** + * Set the colour map of the image. + * @param {Object} map The colour map of the image. + */ + this.setColourMap = function(map) { + colourMap = map; + this.fireEvent({"type": "colour-change", + "wc": this.getCurrentWindowLut().getWindowLevel().getCenter(), + "ww": this.getCurrentWindowLut().getWindowLevel().getWidth() }); + }; + + /** + * Get the current position. + * @return {Object} The current position. + */ + this.getCurrentPosition = function() { + // return a clone to avoid reference problems + return {"i": currentPosition.i, "j": currentPosition.j, "k": currentPosition.k}; + }; + /** + * Set the current position. + * @param {Object} pos The current position. + * @param {Boolean} silent If true, does not fire a slice-change event. + * @return {Boolean} False if not in bounds + */ + this.setCurrentPosition = function(pos, silent) { + // default silent flag to false + if ( typeof silent === "undefined" ) { + silent = false; + } + // check if possible + if( !image.getGeometry().getSize().isInBounds(pos.i,pos.j,pos.k) ) { + return false; + } + var oldPosition = currentPosition; + currentPosition = pos; + + // fire a 'position-change' event + if( image.getPhotometricInterpretation().match(/MONOCHROME/) !== null ) + { + this.fireEvent({"type": "position-change", + "i": pos.i, "j": pos.j, "k": pos.k, + "value": image.getRescaledValue(pos.i,pos.j,pos.k, this.getCurrentFrame())}); + } + else + { + this.fireEvent({"type": "position-change", + "i": pos.i, "j": pos.j, "k": pos.k}); + } + + // fire a slice change event (used to trigger redraw) + if ( !silent ) { + if( oldPosition.k !== currentPosition.k ) { + this.fireEvent({"type": "slice-change"}); + } + } + + // all good + return true; + }; + + /** + * Get the current frame number. + * @return {Number} The current frame number. + */ + this.getCurrentFrame = function() { + return currentFrame; + }; + + /** + * Set the current frame number. + * @param {Number} The current frame number. + * @return {Boolean} False if not in bounds + */ + this.setCurrentFrame = function (frame) { + // check if possible + if( frame < 0 || frame >= image.getNumberOfFrames() ) { + return false; + } + // assign + var oldFrame = currentFrame; + currentFrame = frame; + // fire event + if( oldFrame !== currentFrame && image.getNumberOfFrames() !== 1 ) { + this.fireEvent({"type": "frame-change", "frame": currentFrame}); + // silent set current position to update info text + this.setCurrentPosition(this.getCurrentPosition(),true); + } + // all good + return true; + }; + + /** + * Append another view to this one. + * @param {Object} rhs The view to append. + */ + this.append = function( rhs ) + { + // append images + var newSliceNumber = this.getImage().appendSlice( rhs.getImage() ); + // update position if a slice was appended before + if ( newSliceNumber <= this.getCurrentPosition().k ) { + this.setCurrentPosition( + {"i": this.getCurrentPosition().i, + "j": this.getCurrentPosition().j, + "k": this.getCurrentPosition().k + 1}, true ); + } + // add window presets + this.addWindowPresets( rhs.getWindowPresets(), newSliceNumber ); + }; + + /** + * Append a frame buffer to the included image. + * @param {Object} frameBuffer The frame buffer to append. + */ + this.appendFrameBuffer = function (frameBuffer) + { + this.getImage().appendFrameBuffer(frameBuffer); + }; + + /** + * Set the view window/level. + * @param {Number} center The window center. + * @param {Number} width The window width. + * @param {String} name Associated preset name, defaults to 'manual'. + * Warning: uses the latest set rescale LUT or the default linear one. + */ + this.setWindowLevel = function ( center, width, name ) + { + // window width shall be >= 1 (see https://www.dabsoft.ch/dicom/3/C.11.2.1.2/) + if ( width >= 1 ) { + + // get current window/level (before updating name) + var sliceNumber = this.getCurrentPosition().k; + var currentWl = null; + var rsi = image.getRescaleSlopeAndIntercept(sliceNumber); + if ( rsi && typeof rsi !== "undefined" ) { + var currentLut = windowLuts[ rsi.toString() ]; + if ( currentLut && typeof currentLut !== "undefined") { + currentWl = currentLut.getWindowLevel(); + } + } + + if ( typeof name === "undefined" ) { + name = "manual"; + } + // update current preset name + currentPresetName = name; + + var wl = new dwv.image.WindowLevel(center, width); + var keys = Object.keys(windowLuts); + + // create the first lut if none exists + if (keys.length === 0) { + // create the rescale lookup table + var rescaleLut = new dwv.image.lut.Rescale( + image.getRescaleSlopeAndIntercept(0), image.getMeta().BitsStored ); + // create the window lookup table + var windowLut = new dwv.image.lut.Window(rescaleLut, image.getMeta().IsSigned); + this.addWindowLut(windowLut); + } + + // set window level on luts + for ( var key in windowLuts ) { + windowLuts[key].setWindowLevel(wl); + } + + // fire window level change event + if (currentWl && typeof currentWl !== "undefined") { + if (currentWl.getWidth() !== width) { + this.fireEvent({"type": "wl-width-change", "wc": center, "ww": width }); + } + if (currentWl.getCenter() !== center) { + this.fireEvent({"type": "wl-center-change", "wc": center, "ww": width }); + } + } else { + this.fireEvent({"type": "wl-width-change", "wc": center, "ww": width }); + this.fireEvent({"type": "wl-center-change", "wc": center, "ww": width }); + } + } + }; + + /** + * Set the window level to the preset with the input name. + * @param {String} name The name of the preset to activate. + */ + this.setWindowLevelPreset = function (name) { + var preset = this.getWindowPresets()[name]; + if ( typeof preset === "undefined" ) { + throw new Error("Unknown window level preset: '" + name + "'"); + } + // special min/max + if (name === "minmax" && typeof preset.wl === "undefined") { + preset.wl = this.getWindowLevelMinMax(); + } + // special 'perslice' case + if (typeof preset.perslice !== "undefined" && + preset.perslice === true) { + preset = { "wl": preset.wl[this.getCurrentPosition().k] }; + } + // set w/l + this.setWindowLevel( preset.wl.getCenter(), preset.wl.getWidth(), name ); + }; + + /** + * Set the window level to the preset with the input id. + * @param {Number} id The id of the preset to activate. + */ + this.setWindowLevelPresetById = function (id) { + var keys = Object.keys(this.getWindowPresets()); + this.setWindowLevelPreset( keys[id] ); + }; + + /** + * Clone the image using all meta data and the original data buffer. + * @return {View} A full copy of this {dwv.image.View}. + */ + this.clone = function () + { + var copy = new dwv.image.View(this.getImage()); + for ( var key in windowLuts ) { + copy.addWindowLut(windowLuts[key]); + } + copy.setListeners(this.getListeners()); + return copy; + }; + + /** + * View listeners + * @private + * @type Object + */ + var listeners = {}; + /** + * Get the view listeners. + * @return {Object} The view listeners. + */ + this.getListeners = function() { return listeners; }; + /** + * Set the view listeners. + * @param {Object} list The view listeners. + */ + this.setListeners = function(list) { listeners = list; }; +}; + +/** + * Get the image window/level that covers the full data range. + * Warning: uses the latest set rescale LUT or the default linear one. + */ +dwv.image.View.prototype.getWindowLevelMinMax = function () +{ + var range = this.getImage().getRescaledDataRange(); + var min = range.min; + var max = range.max; + var width = max - min; + var center = min + width/2; + return new dwv.image.WindowLevel(center, width); +}; + +/** + * Set the image window/level to cover the full data range. + * Warning: uses the latest set rescale LUT or the default linear one. + */ +dwv.image.View.prototype.setWindowLevelMinMax = function() +{ + // calculate center and width + var wl = this.getWindowLevelMinMax(); + // set window level + this.setWindowLevel(wl.getCenter(), wl.getWidth(), "minmax"); +}; + +/** + * Generate display image data to be given to a canvas. + * @param {Array} array The array to fill in. + */ +dwv.image.View.prototype.generateImageData = function( array ) +{ + var windowLut = this.getCurrentWindowLut(); + + var image = this.getImage(); + var sliceSize = image.getGeometry().getSize().getSliceSize(); + var sliceOffset = sliceSize * this.getCurrentPosition().k; + var frame = (this.getCurrentFrame()) ? this.getCurrentFrame() : 0; + + var index = 0; + var pxValue = 0; + var stepPos = 0; + + var photoInterpretation = image.getPhotometricInterpretation(); + switch (photoInterpretation) + { + case "MONOCHROME1": + case "MONOCHROME2": + var colourMap = this.getColourMap(); + var iMax = sliceOffset + sliceSize; + for(var i=sliceOffset; i < iMax; ++i) + { + pxValue = parseInt( windowLut.getValue( + image.getValueAtOffset(i, frame) ), 10 ); + array.data[index] = colourMap.red[pxValue]; + array.data[index+1] = colourMap.green[pxValue]; + array.data[index+2] = colourMap.blue[pxValue]; + array.data[index+3] = 0xff; + index += 4; + } + break; + + case "RGB": + // 3 times bigger... + sliceOffset *= 3; + // the planar configuration defines the memory layout + var planarConfig = image.getPlanarConfiguration(); + if( planarConfig !== 0 && planarConfig !== 1 ) { + throw new Error("Unsupported planar configuration: "+planarConfig); + } + // default: RGBRGBRGBRGB... + var posR = sliceOffset; + var posG = sliceOffset + 1; + var posB = sliceOffset + 2; + stepPos = 3; + // RRRR...GGGG...BBBB... + if (planarConfig === 1) { + posR = sliceOffset; + posG = sliceOffset + sliceSize; + posB = sliceOffset + 2 * sliceSize; + stepPos = 1; + } + + for(var j=0; j < sliceSize; ++j) + { + array.data[index] = parseInt( windowLut.getValue( + image.getValueAtOffset(posR, frame) ), 10 ); + array.data[index+1] = parseInt( windowLut.getValue( + image.getValueAtOffset(posG, frame) ), 10 ); + array.data[index+2] = parseInt( windowLut.getValue( + image.getValueAtOffset(posB, frame) ), 10 ); + array.data[index+3] = 0xff; + index += 4; + + posR += stepPos; + posG += stepPos; + posB += stepPos; + } + break; + + case "YBR_FULL_422": + // theory: + // http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.7.html#sect_C.7.6.3.1.2 + // reverse equation: + // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion + + // 3 times bigger... + sliceOffset *= 3; + // the planar configuration defines the memory layout + var planarConfigYBR = image.getPlanarConfiguration(); + if( planarConfigYBR !== 0 && planarConfigYBR !== 1 ) { + throw new Error("Unsupported planar configuration: "+planarConfigYBR); + } + // default: YBRYBRYBR... + var posY = sliceOffset; + var posCB = sliceOffset + 1; + var posCR = sliceOffset + 2; + stepPos = 3; + // YYYY...BBBB...RRRR... + if (planarConfigYBR === 1) { + posY = sliceOffset; + posCB = sliceOffset + sliceSize; + posCR = sliceOffset + 2 * sliceSize; + stepPos = 1; + } + + var y, cb, cr; + var r, g, b; + for (var k=0; k < sliceSize; ++k) + { + y = image.getValueAtOffset(posY, frame); + cb = image.getValueAtOffset(posCB, frame); + cr = image.getValueAtOffset(posCR, frame); + + r = y + 1.402 * (cr - 128); + g = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128); + b = y + 1.772 * (cb - 128); + + array.data[index] = parseInt( windowLut.getValue(r), 10 ); + array.data[index+1] = parseInt( windowLut.getValue(g), 10 ); + array.data[index+2] = parseInt( windowLut.getValue(b), 10 ); + array.data[index+3] = 0xff; + index += 4; + + posY += stepPos; + posCB += stepPos; + posCR += stepPos; + } + break; + + default: + throw new Error("Unsupported photometric interpretation: "+photoInterpretation); + } +}; + +/** + * Add an event listener on the view. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ +dwv.image.View.prototype.addEventListener = function(type, listener) +{ + var listeners = this.getListeners(); + if( !listeners[type] ) { + listeners[type] = []; + } + listeners[type].push(listener); +}; + +/** + * Remove an event listener on the view. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ +dwv.image.View.prototype.removeEventListener = function(type, listener) +{ + var listeners = this.getListeners(); + if( !listeners[type] ) { + return; + } + for(var i=0; i < listeners[type].length; ++i) + { + if( listeners[type][i] === listener ) { + listeners[type].splice(i,1); + } + } +}; + +/** + * Fire an event: call all associated listeners. + * @param {Object} event The event to fire. + */ +dwv.image.View.prototype.fireEvent = function(event) +{ + var listeners = this.getListeners(); + if( !listeners[event.type] ) { + return; + } + for(var i=0; i < listeners[event.type].length; ++i) + { + listeners[event.type][i](event); + } +}; + +/** + * View factory. + * @constructor + */ +dwv.image.ViewFactory = function () {}; + +/** + * Get an View object from the read DICOM file. + * @param {Object} dicomElements The DICOM tags. + * @param {Object} image The associated image. + * @return {View} The new View. + */ +dwv.image.ViewFactory.prototype.create = function (dicomElements, image) +{ + // view + var view = new dwv.image.View(image); + + // default color map + if( image.getPhotometricInterpretation() === "MONOCHROME1") { + view.setDefaultColourMap(dwv.image.lut.invPlain); + } + + // presets + var windowPresets = {}; + + // DICOM presets + var windowCenter = dicomElements.getFromKey("x00281050", true); + var windowWidth = dicomElements.getFromKey("x00281051", true); + var windowCWExplanation = dicomElements.getFromKey("x00281055", true); + if ( windowCenter && windowWidth ) { + var name; + for ( var j = 0; j < windowCenter.length; ++j) { + var center = parseFloat( windowCenter[j], 10 ); + var width = parseFloat( windowWidth[j], 10 ); + if ( center && width ) { + name = ""; + if ( windowCWExplanation ) { + name = dwv.dicom.cleanString(windowCWExplanation[j]); + } + if (name === "") { + name = "Default"+j; + } + windowPresets[name] = { + "wl": [new dwv.image.WindowLevel(center, width)], + "name": name, + "perslice": true}; + } + } + } + + // min/max + // Not filled yet since it is stil too costly to calculate min/max + // for each slice... It will be filled at first use (see view.setWindowLevelPreset). + // Order is important, if no wl from DICOM, this will be the default. + windowPresets.minmax = { "name": "minmax" }; + + // optional modality presets + if ( typeof dwv.tool.defaultpresets !== "undefined" ) { + var modality = image.getMeta().Modality; + for( var key in dwv.tool.defaultpresets[modality] ) { + var preset = dwv.tool.defaultpresets[modality][key]; + windowPresets[key] = { + "wl": new dwv.image.WindowLevel(preset.center, preset.width), + "name": key}; + } + } + + // store + view.setWindowPresets( windowPresets ); + + return view; +}; + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * DICOM data loader. + */ +dwv.io.DicomDataLoader = function () +{ + // closure to self + var self = this; + + /** + * Loader options. + * @private + * @type Object + */ + var options = {}; + + /** + * Loading flag. + * @private + * @type Boolean + */ + var isLoading = false; + + /** + * Set the loader options. + * @param {Object} opt The input options. + */ + this.setOptions = function (opt) { + options = opt; + }; + + /** + * Is the load ongoing? + * @return {Boolean} True if loading. + */ + this.isLoading = function () { + return isLoading; + }; + + /** + * DICOM buffer to dwv.image.View (asynchronous) + */ + var db2v = new dwv.image.DicomBufferToView(); + + /** + * Load data. + * @param {Object} buffer The DICOM buffer. + * @param {String} origin The data origin. + * @param {Number} index The data index. + */ + this.load = function (buffer, origin, index) { + // set loading flag + isLoading = true; + // set character set + if (typeof options.defaultCharacterSet !== "undefined") { + db2v.setDefaultCharacterSet(options.defaultCharacterSet); + } + // connect handlers + db2v.onload = self.onload; + db2v.onloadend = function () { + // reset loading flag + isLoading = false; + // call listeners + self.onloadend(); + }; + db2v.onprogress = self.onprogress; + // convert + try { + db2v.convert( buffer, index ); + } catch (error) { + // TODO: error will be for individual file, isLoading is global... + //isLoading = false; + self.onerror(error); + } + }; + + /** + * Abort load. + */ + this.abort = function () { + // abort conversion + db2v.abort(); + // reset loading flag + isLoading = false; + // call listeners + self.onabort({message: "Abort while loading DICOM data."}); + }; + + /** + * Get a file load handler. + * @param {Object} file The file to load. + * @param {Number} index The index 'id' of the file. + * @return {Function} A file load handler. + */ + this.getFileLoadHandler = function (file, index) { + return function (event) { + self.load(event.target.result, file, index); + }; + }; + + /** + * Get a url load handler. + * @param {String} url The url to load. + * @param {Number} index The index 'id' of the url. + * @return {Function} A url load handler. + */ + this.getUrlLoadHandler = function (url, index) { + return function (/*event*/) { + // check response status + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes + // status 200: "OK"; status 0: "debug" + if (this.status !== 200 && this.status !== 0) { + self.onerror({'name': "RequestError", + 'message': "Error status: " + this.status + + " while loading '" + url + "' [DicomDataLoader]" }); + return; + } + // load + self.load(this.response, url, index); + }; + }; + +}; // class DicomDataLoader + +/** + * Check if the loader can load the provided file. + * @param {Object} file The file to check. + * @return True if the file can be loaded. + */ +dwv.io.DicomDataLoader.prototype.canLoadFile = function (file) { + var split = file.name.split('.'); + var ext = ""; + if (split.length !== 1) { + ext = split.pop().toLowerCase(); + } + var hasExt = (ext.length !== 0); + return !hasExt || (ext === "dcm"); +}; + +/** + * Check if the loader can load the provided url. + * @param {String} url The url to check. + * @return True if the url can be loaded. + */ +dwv.io.DicomDataLoader.prototype.canLoadUrl = function (url) { + var split = url.split('.'); + var ext = ""; + if (split.length !== 1) { + ext = split.pop().toLowerCase(); + } + var hasExt = (ext.length !== 0) && (ext.length < 5); + // wado url + var isDicomContentType = (url.indexOf("contentType=application/dicom") !== -1); + + return isDicomContentType || (ext === "dcm") || !hasExt; +}; + +/** + * Get the file content type needed by the loader. + * @return One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.DicomDataLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.ArrayBuffer; +}; + +/** + * Get the url content type needed by the loader. + * @return One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.DicomDataLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.ArrayBuffer; +}; + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.DicomDataLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle an load end event. + * Default does nothing. + */ +dwv.io.DicomDataLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.DicomDataLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.DicomDataLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.DicomDataLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push( "DicomDataLoader" ); + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.io = dwv.io || {}; + +// file content types +dwv.io.fileContentTypes = { + 'Text': 0, + 'ArrayBuffer': 1, + 'DataURL': 2 +}; + +/** + * Files loader. + * @constructor + */ +dwv.io.FilesLoader = function () +{ + /** + * Closure to self. + * @private + * @type Object + */ + var self = this; + + /** + * Array of launched readers (used in abort). + * @private + * @type Array + */ + var readers = []; + + /** + * Launched loader (used in abort). + * @private + * @type Object + */ + var runningLoader = []; + + /** + * Number of data to load. + * @private + * @type Number + */ + var nToLoad = 0; + /** + * Number of loaded data. + * @private + * @type Number + */ + var nLoaded = 0; + + /** + * The default character set (optional). + * @private + * @type String + */ + var defaultCharacterSet; + + /** + * Get the default character set. + * @return {String} The default character set. + */ + this.getDefaultCharacterSet = function () { + return defaultCharacterSet; + }; + + /** + * Set the default character set. + * @param {String} characterSet The character set. + */ + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; + }; + + /** + * Store a launched reader. + * @param {Object} request The launched reader. + */ + this.storeReader = function (reader) { + readers.push(reader); + }; + + /** + * Clear the stored readers. + */ + this.clearStoredReaders = function () { + readers = []; + }; + + /** + * Store the launched loader. + * @param {Object} loader The launched loader. + */ + this.storeLoader = function (loader) { + runningLoader = loader; + }; + + /** + * Clear the stored loader. + */ + this.clearStoredLoader = function () { + runningLoader = null; + }; + + /** + * Abort a URLs load. + */ + this.abort = function () { + // abort readers + for ( var i = 0; i < readers.length; ++i ) { + // 0: EMPTY, 1: LOADING, 2: DONE + if ( readers[i].readyState === 1 ) { + readers[i].abort(); + } + } + this.clearStoredReaders(); + // abort loader + if ( runningLoader ) { + runningLoader.abort(); + } + this.clearStoredLoader(); + }; + + /** + * Set the number of data to load. + * @param {Number} n The number of data to load. + */ + this.setNToLoad = function (n) { + nToLoad = n; + }; + + /** + * Increment the number of loaded data + * and call onloadend if loaded all data. + */ + this.addLoaded = function () { + nLoaded++; + if ( nLoaded === nToLoad ) { + self.onloadend(); + } + }; + +}; // class File + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.FilesLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle a load end event. + * Default does nothing. + */ +dwv.io.FilesLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.FilesLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.FilesLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.FilesLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Load a list of files. + * @param {Array} ioArray The list of files to load. + * @external FileReader + */ +dwv.io.FilesLoader.prototype.load = function (ioArray) +{ + // clear storage + this.clearStoredReaders(); + this.clearStoredLoader(); + + // closure to self for handlers + var self = this; + // set the number of data to load + this.setNToLoad( ioArray.length ); + + var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress); + mproghandler.setNToLoad( ioArray.length ); + + // get loaders + var loaders = []; + for (var m = 0; m < dwv.io.loaderList.length; ++m) { + loaders.push( new dwv.io[dwv.io.loaderList[m]]() ); + } + + // set loaders callbacks + var loader = null; + for (var k = 0; k < loaders.length; ++k) { + loader = loaders[k]; + loader.onload = self.onload; + loader.onloadend = self.addLoaded; + loader.onerror = self.onerror; + loader.onabort = self.onabort; + loader.setOptions({ + 'defaultCharacterSet': this.getDefaultCharacterSet() + }); + loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1); + } + + // request onerror handler + var getReaderOnError = function (origin) { + return function (event) { + var message = "An error occurred while reading '" + origin + "'"; + if (typeof event.getMessage !== "undefined") { + message += " (" + event.getMessage() + ")"; + } + message += "."; + self.onerror( {'name': "FileReaderError", 'message': message } ); + }; + }; + + // request onabort handler + var getReaderOnAbort = function (origin) { + return function () { + self.onabort( {'message': "Abort while reading '" + origin + "'" } ); + }; + }; + + // loop on I/O elements + for (var i = 0; i < ioArray.length; ++i) + { + var file = ioArray[i]; + var reader = new FileReader(); + + // store reader + this.storeReader(reader); + + // bind reader progress + reader.onprogress = mproghandler.getMonoProgressHandler(i, 0); + + // find a loader + var foundLoader = false; + for (var l = 0; l < loaders.length; ++l) { + loader = loaders[l]; + if (loader.canLoadFile(file)) { + foundLoader = true; + // store loader + this.storeLoader(loader); + // set reader callbacks + reader.onload = loader.getFileLoadHandler(file, i); + reader.onerror = getReaderOnError(file.name); + reader.onabort = getReaderOnAbort(file.name); + // read + if (loader.loadFileAs() === dwv.io.fileContentTypes.Text) { + reader.readAsText(file); + } else if (loader.loadFileAs() === dwv.io.fileContentTypes.DataURL) { + reader.readAsDataURL(file); + } else if (loader.loadFileAs() === dwv.io.fileContentTypes.ArrayBuffer) { + reader.readAsArrayBuffer(file); + } + // next file + break; + } + } + // TODO: throw? + if (!foundLoader) { + throw new Error("No loader found for file: "+file); + } + } +}; + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * JSON text loader. + */ +dwv.io.JSONTextLoader = function () +{ + // closure to self + var self = this; + + /** + * Loading flag. + * @private + * @type Boolean + */ + var isLoading = false; + + /** + * Set the loader options. + * @param {Object} opt The input options. + */ + this.setOptions = function () { + // does nothing + }; + + /** + * Is the load ongoing? + * @return {Boolean} True if loading. + */ + this.isLoading = function () { + return isLoading; + }; + + /** + * Load data. + * @param {Object} text The input text. + * @param {String} origin The data origin. + * @param {Number} index The data index. + */ + this.load = function (text, origin, index) { + // set loading flag + isLoading = true; + try { + self.onload( text ); + // reset loading flag + isLoading = false; + // call listeners + self.onloadend(); + } catch (error) { + self.onerror(error); + } + self.onprogress({'type': 'read-progress', 'lengthComputable': true, + 'loaded': 100, 'total': 100, 'index': index}); + }; + + /** + * Abort load: pass to listeners. + */ + this.abort = function () { + // reset loading flag + isLoading = false; + // call listeners + self.onabort(); + }; + + /** + * Get a file load handler. + * @param {Object} file The file to load. + * @param {Number} index The index 'id' of the file. + * @return {Function} A file load handler. + */ + this.getFileLoadHandler = function (file, index) { + return function (event) { + self.load(event.target.result, file, index); + }; + }; + + /** + * Get a url load handler. + * @param {String} url The url to load. + * @param {Number} index The index 'id' of the url. + * @return {Function} A url load handler. + */ + this.getUrlLoadHandler = function (url, index) { + return function (/*event*/) { + // check response status + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes + // status 200: "OK"; status 0: "debug" + if (this.status !== 200 && this.status !== 0) { + self.onerror({'name': "RequestError", + 'message': "Error status: " + this.status + + " while loading '" + url + "' [JSONTextLoader]" }); + return; + } + // load + self.load(this.responseText, url, index); + }; + }; + +}; // class JSONTextLoader + +/** + * Check if the loader can load the provided file. + * @param {Object} file The file to check. + * @return True if the file can be loaded. + */ +dwv.io.JSONTextLoader.prototype.canLoadFile = function (file) { + var ext = file.name.split('.').pop().toLowerCase(); + return (ext === "json"); +}; + +/** + * Check if the loader can load the provided url. + * @param {String} url The url to check. + * @return True if the url can be loaded. + */ +dwv.io.JSONTextLoader.prototype.canLoadUrl = function (url) { + var ext = url.split('.').pop().toLowerCase(); + return (ext === "json"); +}; + +/** + * Get the file content type needed by the loader. + * @return One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.JSONTextLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.Text; +}; + +/** + * Get the url content type needed by the loader. + * @return One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.JSONTextLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.Text; +}; + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.JSONTextLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle an load end event. + * Default does nothing. + */ +dwv.io.JSONTextLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.JSONTextLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.JSONTextLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.JSONTextLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push( "JSONTextLoader" ); + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * Memory loader. + * @constructor + */ +dwv.io.MemoryLoader = function () +{ + /** + * Closure to self. + * @private + * @type Object + */ + var self = this; + + /** + * Launched loader (used in abort). + * @private + * @type Object + */ + var runningLoader = null; + + /** + * Number of data to load. + * @private + * @type Number + */ + var nToLoad = 0; + /** + * Number of loaded data. + * @private + * @type Number + */ + var nLoaded = 0; + + /** + * The default character set (optional). + * @private + * @type String + */ + var defaultCharacterSet; + + /** + * Get the default character set. + * @return {String} The default character set. + */ + this.getDefaultCharacterSet = function () { + return defaultCharacterSet; + }; + + /** + * Set the default character set. + * @param {String} characterSet The character set. + */ + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; + }; + + /** + * Store a launched loader. + * @param {Object} loader The launched loader. + */ + this.storeLoader = function (loader) { + runningLoader = loader; + }; + + /** + * Clear the stored loader. + */ + this.clearStoredLoader = function () { + runningLoader = null; + }; + + /** + * Abort a memory load. + */ + this.abort = function () { + // abort loader + runningLoader.abort(); + this.clearStoredLoaders(); + }; + + /** + * Set the number of data to load. + * @param {Number} n The number of data to load. + */ + this.setNToLoad = function (n) { + nToLoad = n; + }; + + /** + * Increment the number of loaded data + * and call onloadend if loaded all data. + */ + this.addLoaded = function () { + nLoaded++; + if ( nLoaded === nToLoad ) { + self.onloadend(); + } + }; + +}; // class Memory + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.MemoryLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle a load end event. + * Default does nothing. + */ +dwv.io.MemoryLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.MemoryLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.MemoryLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.MemoryLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Load a list of buffers. + * @param {Array} ioArray The list of buffers to load. + */ +dwv.io.MemoryLoader.prototype.load = function (ioArray) +{ + // clear storage + this.clearStoredLoader(); + + // closure to self for handlers + var self = this; + // set the number of data to load + this.setNToLoad( ioArray.length ); + + var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress); + mproghandler.setNToLoad( ioArray.length ); + + // get loaders + var loaders = []; + for (var m = 0; m < dwv.io.loaderList.length; ++m) { + loaders.push( new dwv.io[dwv.io.loaderList[m]]() ); + } + + // set loaders callbacks + var loader = null; + for (var k = 0; k < loaders.length; ++k) { + loader = loaders[k]; + loader.onload = self.onload; + loader.onloadend = self.addLoaded; + loader.onerror = self.onerror; + loader.onabort = self.onabort; + loader.setOptions({ + 'defaultCharacterSet': this.getDefaultCharacterSet() + }); + loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1); + } + + // loop on I/O elements + for (var i = 0; i < ioArray.length; ++i) + { + var iodata = ioArray[i]; + + // find a loader + var foundLoader = false; + for (var l = 0; l < loaders.length; ++l) { + loader = loaders[l]; + if (loader.canLoadUrl(iodata.filename)) { + foundLoader = true; + // store loader + this.storeLoader(loader); + // read + loader.load(iodata.data, iodata.filename, i); + // next file + break; + } + } + // TODO: throw? + if (!foundLoader) { + throw new Error("No loader found for file: "+iodata.filename); + } + } +}; + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * Raw image loader. + */ +dwv.io.RawImageLoader = function () +{ + // closure to self + var self = this; + + /** + * Set the loader options. + * @param {Object} opt The input options. + */ + this.setOptions = function () { + // does nothing + }; + + /** + * Is the load ongoing? TODO... + * @return {Boolean} True if loading. + */ + this.isLoading = function () { + return true; + }; + + /** + * Create a Data URI from an HTTP request response. + * @param {Object} response The HTTP request response. + * @param {String} dataType The data type. + */ + function createDataUri(response, dataType) { + // image data as string + var bytes = new Uint8Array(response); + var imageDataStr = ''; + for( var i = 0; i < bytes.byteLength; ++i ) { + imageDataStr += String.fromCharCode(bytes[i]); + } + // image type + var imageType = dataType; + if (imageType === "jpg") { + imageType = "jpeg"; + } + // create uri + var uri = "data:image/" + imageType + ";base64," + window.btoa(imageDataStr); + return uri; + } + + /** + * Load data. + * @param {Object} dataUri The data URI. + * @param {String} origin The data origin. + * @param {Number} index The data index. + */ + this.load = function ( dataUri, origin, index ) { + // create a DOM image + var image = new Image(); + image.src = dataUri; + // storing values to pass them on + image.origin = origin; + image.index = index; + // triggered by ctx.drawImage + image.onload = function (/*event*/) { + try { + self.onload( dwv.image.getViewFromDOMImage(this) ); + self.onloadend(); + } catch (error) { + self.onerror(error); + } + self.onprogress({'type': 'read-progress', 'lengthComputable': true, + 'loaded': 100, 'total': 100, 'index': index}); + }; + }; + + /** + * Abort load. TODO... + */ + this.abort = function () { + self.onabort(); + }; + + /** + * Get a file load handler. + * @param {Object} file The file to load. + * @param {Number} index The index 'id' of the file. + * @return {Function} A file load handler. + */ + this.getFileLoadHandler = function (file, index) { + return function (event) { + self.load(event.target.result, file, index); + }; + }; + + /** + * Get a url load handler. + * @param {String} url The url to load. + * @param {Number} index The index 'id' of the url. + * @return {Function} A url load handler. + */ + this.getUrlLoadHandler = function (url, index) { + return function (/*event*/) { + // check response status + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes + // status 200: "OK"; status 0: "debug" + if (this.status !== 200 && this.status !== 0) { + self.onerror({'name': "RequestError", + 'message': "Error status: " + this.status + + " while loading '" + url + "' [RawImageLoader]" }); + return; + } + // load + var ext = url.split('.').pop().toLowerCase(); + self.load(createDataUri(this.response, ext), url, index); + }; + }; + +}; // class RawImageLoader + +/** + * Check if the loader can load the provided file. + * @param {Object} file The file to check. + * @return True if the file can be loaded. + */ +dwv.io.RawImageLoader.prototype.canLoadFile = function (file) { + return file.type.match("image.*"); +}; + +/** + * Check if the loader can load the provided url. + * @param {String} url The url to check. + * @return True if the url can be loaded. + */ +dwv.io.RawImageLoader.prototype.canLoadUrl = function (url) { + var ext = url.split('.').pop().toLowerCase(); + var hasImageExt = (ext === "jpeg") || (ext === "jpg") || + (ext === "png") || (ext === "gif"); + // wado url + var isImageContentType = (url.indexOf("contentType=image/jpeg") !== -1) || + (url.indexOf("contentType=image/png") !== -1) || + (url.indexOf("contentType=image/gif") !== -1); + + return isImageContentType || hasImageExt; +}; + +/** + * Get the file content type needed by the loader. + * @return One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.RawImageLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.DataURL; +}; + +/** + * Get the url content type needed by the loader. + * @return One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.RawImageLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.ArrayBuffer; +}; + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.RawImageLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle an load end event. + * Default does nothing. + */ +dwv.io.RawImageLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.RawImageLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.RawImageLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.RawImageLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push( "RawImageLoader" ); + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * Raw video loader. + * url example (cors enabled): + * https://raw.githubusercontent.com/clappr/clappr/master/test/fixtures/SampleVideo_360x240_1mb.mp4 + */ +dwv.io.RawVideoLoader = function () +{ + // closure to self + var self = this; + + /** + * Set the loader options. + * @param {Object} opt The input options. + */ + this.setOptions = function () { + // does nothing + }; + + /** + * Is the load ongoing? TODO... + * @return {Boolean} True if loading. + */ + this.isLoading = function () { + return true; + }; + + /** + * Create a Data URI from an HTTP request response. + * @param {Object} response The HTTP request response. + * @param {String} dataType The data type. + */ + function createDataUri(response, dataType) { + // image data as string + var bytes = new Uint8Array(response); + var videoDataStr = ''; + for( var i = 0; i < bytes.byteLength; ++i ) { + videoDataStr += String.fromCharCode(bytes[i]); + } + // create uri + var uri = "data:video/" + dataType + ";base64," + window.btoa(videoDataStr); + return uri; + } + + /** + * Internal Data URI load. + * @param {Object} dataUri The data URI. + * @param {String} origin The data origin. + * @param {Number} index The data index. + */ + this.load = function ( dataUri, origin, index ) { + // create a DOM video + var video = document.createElement('video'); + video.src = dataUri; + // storing values to pass them on + video.file = origin; + video.index = index; + // onload handler + video.onloadedmetadata = function (/*event*/) { + try { + dwv.image.getViewFromDOMVideo(this, + self.onload, self.onprogress, self.onloadend, index); + } catch (error) { + self.onerror(error); + } + }; + }; + + /** + * Abort load. TODO... + */ + this.abort = function () { + self.onabort(); + }; + + /** + * Get a file load handler. + * @param {Object} file The file to load. + * @param {Number} index The index 'id' of the file. + * @return {Function} A file load handler. + */ + this.getFileLoadHandler = function (file, index) { + return function (event) { + self.load(event.target.result, file, index); + }; + }; + + /** + * Get a url load handler. + * @param {String} url The url to load. + * @param {Number} index The index 'id' of the url. + * @return {Function} A url load handler. + */ + this.getUrlLoadHandler = function (url, index) { + return function (/*event*/) { + // check response status + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes + // status 200: "OK"; status 0: "debug" + if (this.status !== 200 && this.status !== 0) { + self.onerror({'name': "RequestError", + 'message': "Error status: " + this.status + + " while loading '" + url + "' [RawVideoLoader]" }); + return; + } + // load + var ext = url.split('.').pop().toLowerCase(); + self.load(createDataUri(this.response, ext), url, index); + }; + }; + +}; // class RawVideoLoader + +/** + * Check if the loader can load the provided file. + * @param {Object} file The file to check. + * @return True if the file can be loaded. + */ +dwv.io.RawVideoLoader.prototype.canLoadFile = function (file) { + return file.type.match("video.*"); +}; + +/** + * Check if the loader can load the provided url. + * @param {String} url The url to check. + * @return True if the url can be loaded. + */ +dwv.io.RawVideoLoader.prototype.canLoadUrl = function (url) { + var ext = url.split('.').pop().toLowerCase(); + return (ext === "mp4") || (ext === "ogg") || + (ext === "webm"); +}; + +/** + * Get the file content type needed by the loader. + * @return One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.RawVideoLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.DataURL; +}; + +/** + * Get the url content type needed by the loader. + * @return One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.RawVideoLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.ArrayBuffer; +}; + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.RawVideoLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle an load end event. + * Default does nothing. + */ +dwv.io.RawVideoLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.RawVideoLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.RawVideoLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.RawVideoLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push( "RawVideoLoader" ); + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +// url content types +dwv.io.urlContentTypes = { + 'Text': 0, + 'ArrayBuffer': 1, + 'oups': 2 +}; + +/** + * Urls loader. + * @constructor + */ +dwv.io.UrlsLoader = function () +{ + /** + * Closure to self. + * @private + * @type Object + */ + var self = this; + + /** + * Array of launched requests used in abort. + * @private + * @type Array + */ + var requests = []; + + /** + * Launched loader used in abort. + * @private + * @type Object + */ + var runningLoader = null; + + /** + * Number of data to load. + * @private + * @type Number + */ + var nToLoad = 0; + /** + * Number of loaded data. + * @private + * @type Number + */ + var nLoaded = 0; + + /** + * The default character set (optional). + * @private + * @type String + */ + var defaultCharacterSet; + + /** + * Get the default character set. + * @return {String} The default character set. + */ + this.getDefaultCharacterSet = function () { + return defaultCharacterSet; + }; + + /** + * Set the default character set. + * @param {String} characterSet The character set. + */ + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; + }; + + /** + * Store a launched request. + * @param {Object} request The launched request. + */ + this.storeRequest = function (request) { + requests.push(request); + }; + + /** + * Clear the stored requests. + */ + this.clearStoredRequests = function () { + requests = []; + }; + + /** + * Store a launched loader. + * @param {Object} loader The launched loader. + */ + this.storeLoader = function (loader) { + runningLoader = loader; + }; + + /** + * Clear the stored loader. + */ + this.clearStoredLoader = function () { + runningLoader = null; + }; + + /** + * Abort a URLs load. + */ + this.abort = function () { + // abort requests + for ( var i = 0; i < requests.length; ++i ) { + // 0: UNSENT, 1: OPENED, 2: HEADERS_RECEIVED (send()), 3: LOADING, 4: DONE + if ( requests[i].readyState === 2 || requests[i].readyState === 3 ) { + requests[i].abort(); + } + } + this.clearStoredRequests(); + // abort loader + if ( runningLoader && runningLoader.isLoading() ) { + runningLoader.abort(); + } + this.clearStoredLoader(); + }; + + /** + * Set the number of data to load. + * @param {Number} n The number of data to load. + */ + this.setNToLoad = function (n) { + nToLoad = n; + }; + + /** + * Increment the number of loaded data + * and call onloadend if loaded all data. + */ + this.addLoaded = function () { + nLoaded++; + if ( nLoaded === nToLoad ) { + self.onloadend(); + } + }; + +}; // class Url + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.UrlsLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle a load end event. + * Default does nothing. + */ +dwv.io.UrlsLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.UrlsLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.UrlsLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.UrlsLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Load a list of URLs. + * @param {Array} ioArray The list of urls to load. + * @param {Object} options Load options. + * @external XMLHttpRequest + */ +dwv.io.UrlsLoader.prototype.load = function (ioArray, options) +{ + // clear storage + this.clearStoredRequests(); + this.clearStoredLoader(); + + // closure to self for handlers + var self = this; + // set the number of data to load + this.setNToLoad( ioArray.length ); + + var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress); + mproghandler.setNToLoad( ioArray.length ); + + // get loaders + var loaders = []; + for (var m = 0; m < dwv.io.loaderList.length; ++m) { + loaders.push( new dwv.io[dwv.io.loaderList[m]]() ); + } + + // set loaders callbacks + var loader = null; + for (var k = 0; k < loaders.length; ++k) { + loader = loaders[k]; + loader.onload = self.onload; + loader.onloadend = self.addLoaded; + loader.onerror = self.onerror; + loader.onabort = self.onabort; + loader.setOptions({ + 'defaultCharacterSet': this.getDefaultCharacterSet() + }); + loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1); + } + + // request onerror handler + var getRequestOnError = function (origin) { + return function (/*event*/) { + var message = "An error occurred while downloading '" + origin + "'"; + if (typeof this.status !== "undefined") { + message += " (http status: " + this.status + ")"; + } + message += "."; + self.onerror( {'name': "RequestError", 'message': message } ); + }; + }; + + // request onabort handler + var getRequestOnAbort = function (origin) { + return function () { + self.onabort( {'message': "Abort while downloading '" + origin + "'." } ); + }; + }; + + // loop on I/O elements + for (var i = 0; i < ioArray.length; ++i) + { + var url = ioArray[i]; + var request = new XMLHttpRequest(); + request.open('GET', url, true); + + // store request + this.storeRequest(request); + + // optional request headers + if ( typeof options.requestHeaders !== "undefined" ) { + var requestHeaders = options.requestHeaders; + for (var j = 0; j < requestHeaders.length; ++j) { + if ( typeof requestHeaders[j].name !== "undefined" && + typeof requestHeaders[j].value !== "undefined" ) { + request.setRequestHeader(requestHeaders[j].name, requestHeaders[j].value); + } + } + } + + // bind reader progress + request.onprogress = mproghandler.getMonoProgressHandler(i, 0); + request.onloadend = mproghandler.getMonoOnLoadEndHandler(i, 0); + + // find a loader + var foundLoader = false; + for (var l = 0; l < loaders.length; ++l) { + loader = loaders[l]; + if (loader.canLoadUrl(url)) { + foundLoader = true; + // store loader + this.storeLoader(loader); + // set reader callbacks + request.onload = loader.getUrlLoadHandler(url, i); + request.onerror = getRequestOnError(url); + request.onabort = getRequestOnAbort(url); + // response type (default is 'text') + if (loader.loadUrlAs() === dwv.io.urlContentTypes.ArrayBuffer) { + request.responseType = "arraybuffer"; + } + // read + request.send(null); + // next file + break; + } + } + // TODO: throw? + if (!foundLoader) { + throw new Error("No loader found for url: "+url); + } + } +}; + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; +// external +var JSZip = JSZip || {}; + +/** + * ZIP data loader. + */ +dwv.io.ZipLoader = function () +{ + // closure to self + var self = this; + + /** + * Loader options. + * @private + * @type Object + */ + var options = {}; + + /** + * Loading flag. + * @private + * @type Boolean + */ + var isLoading = false; + + /** + * Set the loader options. + * @param {Object} opt The input options. + */ + this.setOptions = function (opt) { + options = opt; + }; + + /** + * Is the load ongoing? + * @return {Boolean} True if loading. + */ + this.isLoading = function () { + return isLoading; + }; + + var filename = ""; + var files = []; + var zobjs = null; + + /** + * JSZip.async callback + * @param {ArrayBuffer} content unzipped file image + * @return {} + */ + function zipAsyncCallback(content) + { + files.push({"filename": filename, "data": content}); + + if (files.length < zobjs.length){ + var num = files.length; + filename = zobjs[num].name; + zobjs[num].async("arrayBuffer").then(zipAsyncCallback); + } + else { + var memoryIO = new dwv.io.MemoryLoader(); + memoryIO.onload = self.onload; + memoryIO.onloadend = function () { + // reset loading flag + isLoading = false; + // call listeners + self.onloadend(); + }; + memoryIO.onprogress = self.onprogress; + memoryIO.onerror = self.onerror; + memoryIO.onabort = self.onabort; + + memoryIO.load(files); + } + } + + /** + * Load data. + * @param {Object} buffer The DICOM buffer. + * @param {String} origin The data origin. + * @param {Number} index The data index. + */ + this.load = function (buffer/*, origin, index*/) { + // set loading flag + isLoading = true; + + JSZip.loadAsync(buffer).then( function(zip) { + files = []; + zobjs = zip.file(/.*\.dcm/); + // recursively load zip files into the files array + var num = files.length; + filename = zobjs[num].name; + zobjs[num].async("arrayBuffer").then(zipAsyncCallback); + }); + }; + + /** + * Abort load: pass to listeners. + */ + this.abort = function () { + // reset loading flag + isLoading = false; + // call listeners + self.onabort(); + }; + + /** + * Get a file load handler. + * @param {Object} file The file to load. + * @param {Number} index The index 'id' of the file. + * @return {Function} A file load handler. + */ + this.getFileLoadHandler = function (file, index) { + return function (event) { + self.load(event.target.result, file, index); + }; + }; + + /** + * Get a url load handler. + * @param {String} url The url to load. + * @param {Number} index The index 'id' of the url. + * @return {Function} A url load handler. + */ + this.getUrlLoadHandler = function (url, index) { + return function (/*event*/) { + // check response status + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes + // status 200: "OK"; status 0: "debug" + if (this.status !== 200 && this.status !== 0) { + self.onerror({'name': "RequestError", + 'message': "Error status: " + this.status + + " while loading '" + url + "' [ZipLoader]" }); + return; + } + // load + self.load(this.response, url, index); + }; + }; + +}; // class DicomDataLoader + +/** + * Check if the loader can load the provided file. + * @param {Object} file The file to check. + * @return True if the file can be loaded. + */ +dwv.io.ZipLoader.prototype.canLoadFile = function (file) { + var ext = file.name.split('.').pop().toLowerCase(); + return (ext === "zip"); +}; + +/** + * Check if the loader can load the provided url. + * @param {String} url The url to check. + * @return True if the url can be loaded. + */ +dwv.io.ZipLoader.prototype.canLoadUrl = function (url) { + var ext = url.split('.').pop().toLowerCase(); + return (ext === "zip"); +}; + +/** + * Get the file content type needed by the loader. + * @return One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.ZipLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.ArrayBuffer; +}; + +/** + * Get the url content type needed by the loader. + * @return One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.ZipLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.ArrayBuffer; +}; + +/** + * Handle a load event. + * @param {Object} event The load event, 'event.target' + * should be the loaded data. + * Default does nothing. + */ +dwv.io.ZipLoader.prototype.onload = function (/*event*/) {}; +/** + * Handle an load end event. + * Default does nothing. + */ +dwv.io.ZipLoader.prototype.onloadend = function () {}; +/** + * Handle a progress event. + * @param {Object} event The progress event. + * Default does nothing. + */ +dwv.io.ZipLoader.prototype.onprogress = function (/*event*/) {}; +/** + * Handle an error event. + * @param {Object} event The error event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.ZipLoader.prototype.onerror = function (/*event*/) {}; +/** + * Handle an abort event. + * @param {Object} event The abort event with an + * optional 'event.message'. + * Default does nothing. + */ +dwv.io.ZipLoader.prototype.onabort = function (/*event*/) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push( "ZipLoader" ); + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.math = dwv.math || {}; + +/** + * Circular Bucket Queue. + * + * Returns input'd points in sorted order. All operations run in roughly O(1) + * time (for input with small cost values), but it has a strict requirement: + * + * If the most recent point had a cost of c, any points added should have a cost + * c' in the range c <= c' <= c + (capacity - 1). + * + * @constructor + * @input bits + * @input cost_functor + */ +dwv.math.BucketQueue = function(bits, cost_functor) +{ + this.bucketCount = 1 << bits; // # of buckets = 2^bits + this.mask = this.bucketCount - 1; // 2^bits - 1 = index mask + this.size = 0; + + this.loc = 0; // Current index in bucket list + + // Cost defaults to item value + this.cost = (typeof(cost_functor) !== 'undefined') ? cost_functor : function(item) { + return item; + }; + + this.buckets = this.buildArray(this.bucketCount); +}; + +dwv.math.BucketQueue.prototype.push = function(item) { + // Prepend item to the list in the appropriate bucket + var bucket = this.getBucket(item); + item.next = this.buckets[bucket]; + this.buckets[bucket] = item; + + this.size++; +}; + +dwv.math.BucketQueue.prototype.pop = function() { + if ( this.size === 0 ) { + throw new Error("Cannot pop, bucketQueue is empty."); + } + + // Find first empty bucket + while ( this.buckets[this.loc] === null ) { + this.loc = (this.loc + 1) % this.bucketCount; + } + + // All items in bucket have same cost, return the first one + var ret = this.buckets[this.loc]; + this.buckets[this.loc] = ret.next; + ret.next = null; + + this.size--; + return ret; +}; + +dwv.math.BucketQueue.prototype.remove = function(item) { + // Tries to remove item from queue. Returns true on success, false otherwise + if ( !item ) { + return false; + } + + // To find node, go to bucket and search through unsorted list. + var bucket = this.getBucket(item); + var node = this.buckets[bucket]; + + while ( node !== null && !item.equals(node.next) ) { + node = node.next; + } + + if ( node === null ) { + // Item not in list, ergo item not in queue + return false; + } + else { + // Found item, do standard list node deletion + node.next = node.next.next; + + this.size--; + return true; + } +}; + +dwv.math.BucketQueue.prototype.isEmpty = function() { + return this.size === 0; +}; + +dwv.math.BucketQueue.prototype.getBucket = function(item) { + // Bucket index is the masked cost + return this.cost(item) & this.mask; +}; + +dwv.math.BucketQueue.prototype.buildArray = function(newSize) { + // Create array and initialze pointers to null + var buckets = new Array(newSize); + + for ( var i = 0; i < buckets.length; i++ ) { + buckets[i] = null; + } + + return buckets; +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Immutable 3x3 Matrix. + * @constructor + */ +dwv.math.Matrix33 = function ( + m00, m01, m02, + m10, m11, m12, + m20, m21, m22 ) +{ + // row-major order + var mat = new Float32Array(9); + mat[0] = m00; mat[1] = m01; mat[2] = m02; + mat[3] = m10; mat[4] = m11; mat[5] = m12; + mat[6] = m20; mat[7] = m21; mat[8] = m22; + + /** + * Get a value of the matrix. + * @param {Number} row The row at wich to get the value. + * @param {Number} col The column at wich to get the value. + */ + this.get = function (row, col) { + return mat[row*3 + col]; + }; +}; // Matrix33 + +/** + * Check for Matrix33 equality. + * @param {Object} rhs The other matrix to compare to. + * @return {Boolean} True if both matrices are equal. + */ +dwv.math.Matrix33.prototype.equals = function (rhs) { + return this.get(0,0) === rhs.get(0,0) && this.get(0,1) === rhs.get(0,1) && + this.get(0,2) === rhs.get(0,2) && this.get(1,0) === rhs.get(1,0) && + this.get(1,1) === rhs.get(1,1) && this.get(1,2) === rhs.get(1,2) && + this.get(2,0) === rhs.get(2,0) && this.get(2,1) === rhs.get(2,1) && + this.get(2,2) === rhs.get(2,2); +}; + +/** + * Get a string representation of the Matrix33. + * @return {String} The matrix as a string. + */ +dwv.math.Matrix33.prototype.toString = function () { + return "[" + this.get(0,0) + ", " + this.get(0,1) + ", " + this.get(0,2) + + "\n " + this.get(1,0) + ", " + this.get(1,1) + ", " + this.get(1,2) + + "\n " + this.get(2,0) + ", " + this.get(2,1) + ", " + this.get(2,2) + "]"; +}; + +/** + * Multiply this matrix by a 3D vector. + * @param {Object} vector3D The input 3D vector + * @return {Object} The result 3D vector + */ +dwv.math.Matrix33.multiplyVector3D = function (vector3D) { + // cache matrix values + var m00 = this.get(0,0); var m01 = this.get(0,1); var m02 = this.get(0,2); + var m10 = this.get(1,0); var m11 = this.get(1,1); var m12 = this.get(1,2); + var m20 = this.get(2,0); var m21 = this.get(2,1); var m22 = this.get(2,2); + // cache vector values + var vx = vector3D.getX(); + var vy = vector3D.getY(); + var vz = vector3D.getZ(); + // calculate + return new dwv.math.Vector3D( + (m00 * vx) + (m01 * vy) + (m02 * vz), + (m10 * vx) + (m11 * vy) + (m12 * vz), + (m20 * vx) + (m21 * vy) + (m22 * vz) ); +}; + +/** + * Create a 3x3 identity matrix. + * @return {Object} The identity matrix. + */ +dwv.math.getIdentityMat33= function () { + return new dwv.math.Matrix33( + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Immutable 2D point. + * @constructor + * @param {Number} x The X coordinate for the point. + * @param {Number} y The Y coordinate for the point. + */ +dwv.math.Point2D = function (x,y) +{ + /** + * Get the X position of the point. + * @return {Number} The X position of the point. + */ + this.getX = function () { return x; }; + /** + * Get the Y position of the point. + * @return {Number} The Y position of the point. + */ + this.getY = function () { return y; }; +}; // Point2D class + +/** + * Check for Point2D equality. + * @param {Object} rhs The other point to compare to. + * @return {Boolean} True if both points are equal. + */ +dwv.math.Point2D.prototype.equals = function (rhs) { + return rhs !== null && + this.getX() === rhs.getX() && + this.getY() === rhs.getY(); +}; + +/** + * Get a string representation of the Point2D. + * @return {String} The point as a string. + */ +dwv.math.Point2D.prototype.toString = function () { + return "(" + this.getX() + ", " + this.getY() + ")"; +}; + +/** + * Get the distance to another Point2D. + * @param {Object} point2D The input point. + */ +dwv.math.Point2D.prototype.getDistance = function (point2D) { + return Math.sqrt( + (this.getX() - point2D.getX()) * (this.getX() - point2D.getX()) + + (this.getY() - point2D.getY()) * (this.getY() - point2D.getY()) ); +}; + +/** + * Mutable 2D point. + * @constructor + * @param {Number} x The X coordinate for the point. + * @param {Number} y The Y coordinate for the point. + */ +dwv.math.FastPoint2D = function (x,y) +{ + this.x = x; + this.y = y; +}; // FastPoint2D class + +/** + * Check for FastPoint2D equality. + * @param {Object} other The other point to compare to. + * @return {Boolean} True if both points are equal. + */ +dwv.math.FastPoint2D.prototype.equals = function (rhs) { + return rhs !== null && + this.x === rhs.x && + this.y === rhs.y; +}; + +/** + * Get a string representation of the FastPoint2D. + * @return {String} The point as a string. + */ +dwv.math.FastPoint2D.prototype.toString = function () { + return "(" + this.x + ", " + this.y + ")"; +}; + +/** + * Immutable 3D point. + * @constructor + * @param {Number} x The X coordinate for the point. + * @param {Number} y The Y coordinate for the point. + * @param {Number} z The Z coordinate for the point. + */ +dwv.math.Point3D = function (x,y,z) +{ + /** + * Get the X position of the point. + * @return {Number} The X position of the point. + */ + this.getX = function () { return x; }; + /** + * Get the Y position of the point. + * @return {Number} The Y position of the point. + */ + this.getY = function () { return y; }; + /** + * Get the Z position of the point. + * @return {Number} The Z position of the point. + */ + this.getZ = function () { return z; }; +}; // Point3D class + +/** + * Check for Point3D equality. + * @param {Object} rhs The other point to compare to. + * @return {Boolean} True if both points are equal. + */ +dwv.math.Point3D.prototype.equals = function (rhs) { + return rhs !== null && + this.getX() === rhs.getX() && + this.getY() === rhs.getY() && + this.getZ() === rhs.getZ(); +}; + +/** + * Get a string representation of the Point3D. + * @return {String} The point as a string. + */ +dwv.math.Point3D.prototype.toString = function () { + return "(" + this.getX() + + ", " + this.getY() + + ", " + this.getZ() + ")"; +}; + +/** + * Get the distance to another Point3D. + * @param {Object} point3D The input point. + */ +dwv.math.Point3D.prototype.getDistance = function (point3D) { + return Math.sqrt( + (this.getX() - point3D.getX()) * (this.getX() - point3D.getX()) + + (this.getY() - point3D.getY()) * (this.getY() - point3D.getY()) + + (this.getZ() - point3D.getZ()) * (this.getZ() - point3D.getZ()) ); +}; + +/** + * Get the difference to another Point3D. + * @param {Object} point3D The input point. + * @return {Object} The 3D vector from the input point to this one. + */ +dwv.math.Point3D.prototype.minus = function (point3D) { + return new dwv.math.Vector3D( + (this.getX() - point3D.getX()), + (this.getY() - point3D.getY()), + (this.getZ() - point3D.getZ()) ); +}; + +/** + * Immutable 3D index. + * @constructor + * @param {Number} i The column index. + * @param {Number} j The row index. + * @param {Number} k The slice index. + */ +dwv.math.Index3D = function (i,j,k) +{ + /** + * Get the column index. + * @return {Number} The column index. + */ + this.getI = function () { return i; }; + /** + * Get the row index. + * @return {Number} The row index. + */ + this.getJ = function () { return j; }; + /** + * Get the slice index. + * @return {Number} The slice index. + */ + this.getK = function () { return k; }; +}; // Index3D class + +/** + * Check for Index3D equality. + * @param {Object} rhs The other index to compare to. + * @return {Boolean} True if both indices are equal. + */ +dwv.math.Index3D.prototype.equals = function (rhs) { + return rhs !== null && + this.getI() === rhs.getI() && + this.getJ() === rhs.getJ() && + this.getK() === rhs.getK(); +}; + +/** + * Get a string representation of the Index3D. + * @return {String} The Index3D as a string. + */ +dwv.math.Index3D.prototype.toString = function () { + return "(" + this.getI() + + ", " + this.getJ() + + ", " + this.getK() + ")"; +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +// Pre-created to reduce allocation in inner loops +var __twothirdpi = ( 2 / (3 * Math.PI) ); + +/** + * + */ +dwv.math.computeGreyscale = function(data, width, height) { + // Returns 2D augmented array containing greyscale data + // Greyscale values found by averaging colour channels + // Input should be in a flat RGBA array, with values between 0 and 255 + var greyscale = []; + + // Compute actual values + for (var y = 0; y < height; y++) { + greyscale[y] = []; + + for (var x = 0; x < width; x++) { + var p = (y*width + x)*4; + greyscale[y][x] = (data[p] + data[p+1] + data[p+2]) / (3*255); + } + } + + // Augment with convenience functions + greyscale.dx = function(x,y) { + if ( x+1 === this[y].length ) { + // If we're at the end, back up one + x--; + } + return this[y][x+1] - this[y][x]; + }; + + greyscale.dy = function(x,y) { + if ( y+1 === this.length ) { + // If we're at the end, back up one + y--; + } + return this[y][x] - this[y+1][x]; + }; + + greyscale.gradMagnitude = function(x,y) { + var dx = this.dx(x,y); + var dy = this.dy(x,y); + return Math.sqrt(dx*dx + dy*dy); + }; + + greyscale.laplace = function(x,y) { + // Laplacian of Gaussian + var lap = -16 * this[y][x]; + lap += this[y-2][x]; + lap += this[y-1][x-1] + 2*this[y-1][x] + this[y-1][x+1]; + lap += this[y][x-2] + 2*this[y][x-1] + 2*this[y][x+1] + this[y][x+2]; + lap += this[y+1][x-1] + 2*this[y+1][x] + this[y+1][x+1]; + lap += this[y+2][x]; + + return lap; + }; + + return greyscale; +}; + +/** + * + */ +dwv.math.computeGradient = function(greyscale) { + // Returns a 2D array of gradient magnitude values for greyscale. The values + // are scaled between 0 and 1, and then flipped, so that it works as a cost + // function. + var gradient = []; + + var max = 0; // Maximum gradient found, for scaling purposes + + var x = 0; + var y = 0; + + for (y = 0; y < greyscale.length-1; y++) { + gradient[y] = []; + + for (x = 0; x < greyscale[y].length-1; x++) { + gradient[y][x] = greyscale.gradMagnitude(x,y); + max = Math.max(gradient[y][x], max); + } + + gradient[y][greyscale[y].length-1] = gradient[y][greyscale.length-2]; + } + + gradient[greyscale.length-1] = []; + for (var i = 0; i < gradient[0].length; i++) { + gradient[greyscale.length-1][i] = gradient[greyscale.length-2][i]; + } + + // Flip and scale. + for (y = 0; y < gradient.length; y++) { + for (x = 0; x < gradient[y].length; x++) { + gradient[y][x] = 1 - (gradient[y][x] / max); + } + } + + return gradient; +}; + +/** + * + */ +dwv.math.computeLaplace = function(greyscale) { + // Returns a 2D array of Laplacian of Gaussian values + var laplace = []; + + // Make the edges low cost here. + + laplace[0] = []; + laplace[1] = []; + for (var i = 1; i < greyscale.length; i++) { + // Pad top, since we can't compute Laplacian + laplace[0][i] = 1; + laplace[1][i] = 1; + } + + for (var y = 2; y < greyscale.length-2; y++) { + laplace[y] = []; + // Pad left, ditto + laplace[y][0] = 1; + laplace[y][1] = 1; + + for (var x = 2; x < greyscale[y].length-2; x++) { + // Threshold needed to get rid of clutter. + laplace[y][x] = (greyscale.laplace(x,y) > 0.33) ? 0 : 1; + } + + // Pad right, ditto + laplace[y][greyscale[y].length-2] = 1; + laplace[y][greyscale[y].length-1] = 1; + } + + laplace[greyscale.length-2] = []; + laplace[greyscale.length-1] = []; + for (var j = 1; j < greyscale.length; j++) { + // Pad bottom, ditto + laplace[greyscale.length-2][j] = 1; + laplace[greyscale.length-1][j] = 1; + } + + return laplace; +}; + +dwv.math.computeGradX = function(greyscale) { + // Returns 2D array of x-gradient values for greyscale + var gradX = []; + + for ( var y = 0; y < greyscale.length; y++ ) { + gradX[y] = []; + + for ( var x = 0; x < greyscale[y].length-1; x++ ) { + gradX[y][x] = greyscale.dx(x,y); + } + + gradX[y][greyscale[y].length-1] = gradX[y][greyscale[y].length-2]; + } + + return gradX; +}; + +dwv.math.computeGradY = function(greyscale) { + // Returns 2D array of y-gradient values for greyscale + var gradY = []; + + for (var y = 0; y < greyscale.length-1; y++) { + gradY[y] = []; + + for ( var x = 0; x < greyscale[y].length; x++ ) { + gradY[y][x] = greyscale.dy(x,y); + } + } + + gradY[greyscale.length-1] = []; + for ( var i = 0; i < greyscale[0].length; i++ ) { + gradY[greyscale.length-1][i] = gradY[greyscale.length-2][i]; + } + + return gradY; +}; + +dwv.math.gradUnitVector = function(gradX, gradY, px, py, out) { + // Returns the gradient vector at (px,py), scaled to a magnitude of 1 + var ox = gradX[py][px]; + var oy = gradY[py][px]; + + var gvm = Math.sqrt(ox*ox + oy*oy); + gvm = Math.max(gvm, 1e-100); // To avoid possible divide-by-0 errors + + out.x = ox / gvm; + out.y = oy / gvm; +}; + +dwv.math.gradDirection = function(gradX, gradY, px, py, qx, qy) { + var __dgpuv = new dwv.math.FastPoint2D(-1, -1); + var __gdquv = new dwv.math.FastPoint2D(-1, -1); + // Compute the gradiant direction, in radians, between to points + dwv.math.gradUnitVector(gradX, gradY, px, py, __dgpuv); + dwv.math.gradUnitVector(gradX, gradY, qx, qy, __gdquv); + + var dp = __dgpuv.y * (qx - px) - __dgpuv.x * (qy - py); + var dq = __gdquv.y * (qx - px) - __gdquv.x * (qy - py); + + // Make sure dp is positive, to keep things consistant + if (dp < 0) { + dp = -dp; + dq = -dq; + } + + if ( px !== qx && py !== qy ) { + // We're going diagonally between pixels + dp *= Math.SQRT1_2; + dq *= Math.SQRT1_2; + } + + return __twothirdpi * (Math.acos(dp) + Math.acos(dq)); +}; + +dwv.math.computeSides = function(dist, gradX, gradY, greyscale) { + // Returns 2 2D arrays, containing inside and outside greyscale values. + // These greyscale values are the intensity just a little bit along the + // gradient vector, in either direction, from the supplied point. These + // values are used when using active-learning Intelligent Scissors + + var sides = {}; + sides.inside = []; + sides.outside = []; + + var guv = new dwv.math.FastPoint2D(-1, -1); // Current gradient unit vector + + for ( var y = 0; y < gradX.length; y++ ) { + sides.inside[y] = []; + sides.outside[y] = []; + + for ( var x = 0; x < gradX[y].length; x++ ) { + dwv.math.gradUnitVector(gradX, gradY, x, y, guv); + + //(x, y) rotated 90 = (y, -x) + + var ix = Math.round(x + dist*guv.y); + var iy = Math.round(y - dist*guv.x); + var ox = Math.round(x - dist*guv.y); + var oy = Math.round(y + dist*guv.x); + + ix = Math.max(Math.min(ix, gradX[y].length-1), 0); + ox = Math.max(Math.min(ox, gradX[y].length-1), 0); + iy = Math.max(Math.min(iy, gradX.length-1), 0); + oy = Math.max(Math.min(oy, gradX.length-1), 0); + + sides.inside[y][x] = greyscale[iy][ix]; + sides.outside[y][x] = greyscale[oy][ox]; + } + } + + return sides; +}; + +dwv.math.gaussianBlur = function(buffer, out) { + // Smooth values over to fill in gaps in the mapping + out[0] = 0.4*buffer[0] + 0.5*buffer[1] + 0.1*buffer[1]; + out[1] = 0.25*buffer[0] + 0.4*buffer[1] + 0.25*buffer[2] + 0.1*buffer[3]; + + for ( var i = 2; i < buffer.length-2; i++ ) { + out[i] = 0.05*buffer[i-2] + 0.25*buffer[i-1] + 0.4*buffer[i] + 0.25*buffer[i+1] + 0.05*buffer[i+2]; + } + + var len = buffer.length; + out[len-2] = 0.25*buffer[len-1] + 0.4*buffer[len-2] + 0.25*buffer[len-3] + 0.1*buffer[len-4]; + out[len-1] = 0.4*buffer[len-1] + 0.5*buffer[len-2] + 0.1*buffer[len-3]; +}; + + +/** + * Scissors + * + * Ref: Eric N. Mortensen, William A. Barrett, Interactive Segmentation with + * Intelligent Scissors, Graphical Models and Image Processing, Volume 60, + * Issue 5, September 1998, Pages 349-384, ISSN 1077-3169, + * DOI: 10.1006/gmip.1998.0480. + * + * {@link http://www.sciencedirect.com/science/article/B6WG4-45JB8WN-9/2/6fe59d8089fd1892c2bfb82283065579} + * + * Highly inspired from {@link http://code.google.com/p/livewire-javascript/} + * @constructor + */ +dwv.math.Scissors = function() +{ + this.width = -1; + this.height = -1; + + this.curPoint = null; // Corrent point we're searching on. + this.searchGranBits = 8; // Bits of resolution for BucketQueue. + this.searchGran = 1 << this.earchGranBits; //bits. + this.pointsPerPost = 500; + + // Precomputed image data. All in ranges 0 >= x >= 1 and all inverted (1 - x). + this.greyscale = null; // Greyscale of image + this.laplace = null; // Laplace zero-crossings (either 0 or 1). + this.gradient = null; // Gradient magnitudes. + this.gradX = null; // X-differences. + this.gradY = null; // Y-differences. + + this.parents = null; // Matrix mapping point => parent along shortest-path to root. + + this.working = false; // Currently computing shortest paths? + + // Begin Training: + this.trained = false; + this.trainingPoints = null; + + this.edgeWidth = 2; + this.trainingLength = 32; + + this.edgeGran = 256; + this.edgeTraining = null; + + this.gradPointsNeeded = 32; + this.gradGran = 1024; + this.gradTraining = null; + + this.insideGran = 256; + this.insideTraining = null; + + this.outsideGran = 256; + this.outsideTraining = null; + // End Training +}; // Scissors class + +// Begin training methods // +dwv.math.Scissors.prototype.getTrainingIdx = function(granularity, value) { + return Math.round((granularity - 1) * value); +}; + +dwv.math.Scissors.prototype.getTrainedEdge = function(edge) { + return this.edgeTraining[this.getTrainingIdx(this.edgeGran, edge)]; +}; + +dwv.math.Scissors.prototype.getTrainedGrad = function(grad) { + return this.gradTraining[this.getTrainingIdx(this.gradGran, grad)]; +}; + +dwv.math.Scissors.prototype.getTrainedInside = function(inside) { + return this.insideTraining[this.getTrainingIdx(this.insideGran, inside)]; +}; + +dwv.math.Scissors.prototype.getTrainedOutside = function(outside) { + return this.outsideTraining[this.getTrainingIdx(this.outsideGran, outside)]; +}; +// End training methods // + +dwv.math.Scissors.prototype.setWorking = function(working) { + // Sets working flag + this.working = working; +}; + +dwv.math.Scissors.prototype.setDimensions = function(width, height) { + this.width = width; + this.height = height; +}; + +dwv.math.Scissors.prototype.setData = function(data) { + if ( this.width === -1 || this.height === -1 ) { + // The width and height should have already been set + throw new Error("Dimensions have not been set."); + } + + this.greyscale = dwv.math.computeGreyscale(data, this.width, this.height); + this.laplace = dwv.math.computeLaplace(this.greyscale); + this.gradient = dwv.math.computeGradient(this.greyscale); + this.gradX = dwv.math.computeGradX(this.greyscale); + this.gradY = dwv.math.computeGradY(this.greyscale); + + var sides = dwv.math.computeSides(this.edgeWidth, this.gradX, this.gradY, this.greyscale); + this.inside = sides.inside; + this.outside = sides.outside; + this.edgeTraining = []; + this.gradTraining = []; + this.insideTraining = []; + this.outsideTraining = []; +}; + +dwv.math.Scissors.prototype.findTrainingPoints = function(p) { + // Grab the last handful of points for training + var points = []; + + if ( this.parents !== null ) { + for ( var i = 0; i < this.trainingLength && p; i++ ) { + points.push(p); + p = this.parents[p.y][p.x]; + } + } + + return points; +}; + +dwv.math.Scissors.prototype.resetTraining = function() { + this.trained = false; // Training is ignored with this flag set +}; + +dwv.math.Scissors.prototype.doTraining = function(p) { + // Compute training weights and measures + this.trainingPoints = this.findTrainingPoints(p); + + if ( this.trainingPoints.length < 8 ) { + return; // Not enough points, I think. It might crash if length = 0. + } + + var buffer = []; + this.calculateTraining(buffer, this.edgeGran, this.greyscale, this.edgeTraining); + this.calculateTraining(buffer, this.gradGran, this.gradient, this.gradTraining); + this.calculateTraining(buffer, this.insideGran, this.inside, this.insideTraining); + this.calculateTraining(buffer, this.outsideGran, this.outside, this.outsideTraining); + + if ( this.trainingPoints.length < this.gradPointsNeeded ) { + // If we have two few training points, the gradient weight map might not + // be smooth enough, so average with normal weights. + this.addInStaticGrad(this.trainingPoints.length, this.gradPointsNeeded); + } + + this.trained = true; +}; + +dwv.math.Scissors.prototype.calculateTraining = function(buffer, granularity, input, output) { + var i = 0; + // Build a map of raw-weights to trained-weights by favoring input values + buffer.length = granularity; + for ( i = 0; i < granularity; i++ ) { + buffer[i] = 0; + } + + var maxVal = 1; + for ( i = 0; i < this.trainingPoints.length; i++ ) { + var p = this.trainingPoints[i]; + var idx = this.getTrainingIdx(granularity, input[p.y][p.x]); + buffer[idx] += 1; + + maxVal = Math.max(maxVal, buffer[idx]); + } + + // Invert and scale. + for ( i = 0; i < granularity; i++ ) { + buffer[i] = 1 - buffer[i] / maxVal; + } + + // Blur it, as suggested. Gets rid of static. + dwv.math.gaussianBlur(buffer, output); +}; + +dwv.math.Scissors.prototype.addInStaticGrad = function(have, need) { + // Average gradient raw-weights to trained-weights map with standard weight + // map so that we don't end up with something to spiky + for ( var i = 0; i < this.gradGran; i++ ) { + this.gradTraining[i] = Math.min(this.gradTraining[i], 1 - i*(need - have)/(need*this.gradGran)); + } +}; + +dwv.math.Scissors.prototype.gradDirection = function(px, py, qx, qy) { + return dwv.math.gradDirection(this.gradX, this.gradY, px, py, qx, qy); +}; + +dwv.math.Scissors.prototype.dist = function(px, py, qx, qy) { + // The grand culmunation of most of the code: the weighted distance function + var grad = this.gradient[qy][qx]; + + if ( px === qx || py === qy ) { + // The distance is Euclidean-ish; non-diagonal edges should be shorter + grad *= Math.SQRT1_2; + } + + var lap = this.laplace[qy][qx]; + var dir = this.gradDirection(px, py, qx, qy); + + if ( this.trained ) { + // Apply training magic + var gradT = this.getTrainedGrad(grad); + var edgeT = this.getTrainedEdge(this.greyscale[py][px]); + var insideT = this.getTrainedInside(this.inside[py][px]); + var outsideT = this.getTrainedOutside(this.outside[py][px]); + + return 0.3*gradT + 0.3*lap + 0.1*(dir + edgeT + insideT + outsideT); + } else { + // Normal weights + return 0.43*grad + 0.43*lap + 0.11*dir; + } +}; + +dwv.math.Scissors.prototype.adj = function(p) { + var list = []; + + var sx = Math.max(p.x-1, 0); + var sy = Math.max(p.y-1, 0); + var ex = Math.min(p.x+1, this.greyscale[0].length-1); + var ey = Math.min(p.y+1, this.greyscale.length-1); + + var idx = 0; + for ( var y = sy; y <= ey; y++ ) { + for ( var x = sx; x <= ex; x++ ) { + if ( x !== p.x || y !== p.y ) { + list[idx++] = new dwv.math.FastPoint2D(x,y); + } + } + } + + return list; +}; + +dwv.math.Scissors.prototype.setPoint = function(sp) { + this.setWorking(true); + + this.curPoint = sp; + + var x = 0; + var y = 0; + + this.visited = []; + for ( y = 0; y < this.height; y++ ) { + this.visited[y] = []; + for ( x = 0; x < this.width; x++ ) { + this.visited[y][x] = false; + } + } + + this.parents = []; + for ( y = 0; y < this.height; y++ ) { + this.parents[y] = []; + } + + this.cost = []; + for ( y = 0; y < this.height; y++ ) { + this.cost[y] = []; + for ( x = 0; x < this.width; x++ ) { + this.cost[y][x] = Number.MAX_VALUE; + } + } + + this.pq = new dwv.math.BucketQueue(this.searchGranBits, function(p) { + return Math.round(this.searchGran * this.costArr[p.y][p.x]); + }); + this.pq.searchGran = this.searchGran; + this.pq.costArr = this.cost; + + this.pq.push(sp); + this.cost[sp.y][sp.x] = 0; +}; + +dwv.math.Scissors.prototype.doWork = function() { + if ( !this.working ) { + return; + } + + this.timeout = null; + + var pointCount = 0; + var newPoints = []; + while ( !this.pq.isEmpty() && pointCount < this.pointsPerPost ) { + var p = this.pq.pop(); + newPoints.push(p); + newPoints.push(this.parents[p.y][p.x]); + + this.visited[p.y][p.x] = true; + + var adjList = this.adj(p); + for ( var i = 0; i < adjList.length; i++) { + var q = adjList[i]; + + var pqCost = this.cost[p.y][p.x] + this.dist(p.x, p.y, q.x, q.y); + + if ( pqCost < this.cost[q.y][q.x] ) { + if ( this.cost[q.y][q.x] !== Number.MAX_VALUE ) { + // Already in PQ, must remove it so we can re-add it. + this.pq.remove(q); + } + + this.cost[q.y][q.x] = pqCost; + this.parents[q.y][q.x] = p; + this.pq.push(q); + } + } + + pointCount++; + } + + return newPoints; +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Mulitply the three inputs if the last two are not null. + * @param {Number} a The first input. + * @param {Number} b The second input. + * @param {Number} c The third input. + * @return {Number} The multiplication of the three inputs or + * null if one of the last two is null. + */ +function mulABC( a, b, c) { + var res = null; + if (b !== null && c !== null) { + res = a * b * c; + } + return res; +} + +/** + * Circle shape. + * @constructor + * @param {Object} centre A Point2D representing the centre of the circle. + * @param {Number} radius The radius of the circle. + */ +dwv.math.Circle = function(centre, radius) +{ + /** + * Circle surface. + * @private + * @type Number + */ + var surface = Math.PI*radius*radius; + + /** + * Get the centre (point) of the circle. + * @return {Object} The center (point) of the circle. + */ + this.getCenter = function() { return centre; }; + /** + * Get the radius of the circle. + * @return {Number} The radius of the circle. + */ + this.getRadius = function() { return radius; }; + /** + * Get the surface of the circle. + * @return {Number} The surface of the circle. + */ + this.getSurface = function() { return surface; }; + /** + * Get the surface of the circle according to a spacing. + * @param {Number} spacingX The X spacing. + * @param {Number} spacingY The Y spacing. + * @return {Number} The surface of the circle multiplied by the given + * spacing or null for null spacings. + */ + this.getWorldSurface = function(spacingX, spacingY) + { + return mulABC(surface, spacingX, spacingY); + }; +}; // Circle class + +/** + * Ellipse shape. + * @constructor + * @param {Object} centre A Point2D representing the centre of the ellipse. + * @param {Number} a The radius of the ellipse on the horizontal axe. + * @param {Number} b The radius of the ellipse on the vertical axe. + */ +dwv.math.Ellipse = function(centre, a, b) +{ + /** + * Circle surface. + * @private + * @type Number + */ + var surface = Math.PI*a*b; + + /** + * Get the centre (point) of the ellipse. + * @return {Object} The center (point) of the ellipse. + */ + this.getCenter = function() { return centre; }; + /** + * Get the radius of the ellipse on the horizontal axe. + * @return {Number} The radius of the ellipse on the horizontal axe. + */ + this.getA = function() { return a; }; + /** + * Get the radius of the ellipse on the vertical axe. + * @return {Number} The radius of the ellipse on the vertical axe. + */ + this.getB = function() { return b; }; + /** + * Get the surface of the ellipse. + * @return {Number} The surface of the ellipse. + */ + this.getSurface = function() { return surface; }; + /** + * Get the surface of the ellipse according to a spacing. + * @param {Number} spacingX The X spacing. + * @param {Number} spacingY The Y spacing. + * @return {Number} The surface of the ellipse multiplied by the given + * spacing or null for null spacings. + */ + this.getWorldSurface = function(spacingX, spacingY) + { + return mulABC(surface, spacingX, spacingY); + }; +}; // Circle class + +/** + * Line shape. + * @constructor + * @param {Object} begin A Point2D representing the beginning of the line. + * @param {Object} end A Point2D representing the end of the line. + */ +dwv.math.Line = function(begin, end) +{ + /** + * Line delta in the X direction. + * @private + * @type Number + */ + var dx = end.getX() - begin.getX(); + /** + * Line delta in the Y direction. + * @private + * @type Number + */ + var dy = end.getY() - begin.getY(); + /** + * Line length. + * @private + * @type Number + */ + var length = Math.sqrt( dx * dx + dy * dy ); + + /** + * Get the begin point of the line. + * @return {Object} The beginning point of the line. + */ + this.getBegin = function() { return begin; }; + /** + * Get the end point of the line. + * @return {Object} The ending point of the line. + */ + this.getEnd = function() { return end; }; + /** + * Get the line delta in the X direction. + * @return {Number} The delta in the X direction. + */ + this.getDeltaX = function() { return dx; }; + /** + * Get the line delta in the Y direction. + * @return {Number} The delta in the Y direction. + */ + this.getDeltaY = function() { return dy; }; + /** + * Get the length of the line. + * @return {Number} The length of the line. + */ + this.getLength = function() { return length; }; + /** + * Get the length of the line according to a spacing. + * @param {Number} spacingX The X spacing. + * @param {Number} spacingY The Y spacing. + * @return {Number} The length of the line with spacing + * or null for null spacings. + */ + this.getWorldLength = function(spacingX, spacingY) + { + var wlen = null; + if (spacingX !== null && spacingY !== null) { + var dxs = dx * spacingX; + var dys = dy * spacingY; + wlen = Math.sqrt( dxs * dxs + dys * dys ); + } + return wlen; + }; + /** + * Get the mid point of the line. + * @return {Object} The mid point of the line. + */ + this.getMidpoint = function() + { + return new dwv.math.Point2D( + parseInt( (begin.getX()+end.getX()) / 2, 10 ), + parseInt( (begin.getY()+end.getY()) / 2, 10 ) ); + }; + /** + * Get the slope of the line. + * @return {Number} The slope of the line. + */ + this.getSlope = function() + { + return dy / dx; + }; + /** + * Get the intercept of the line. + * @return {Number} The slope of the line. + */ + this.getIntercept = function() + { + return (end.getX() * begin.getY() - begin.getX() * end.getY()) / dx; + }; + /** + * Get the inclination of the line. + * @return {Number} The inclination of the line. + */ + this.getInclination = function() + { + // tan(theta) = slope + var angle = Math.atan2( dy, dx ) * 180 / Math.PI; + // shift? + return 180 - angle; + }; +}; // Line class + +/** + * Get the angle between two lines. + * @param line0 The first line. + * @param line1 The second line. + */ +dwv.math.getAngle = function (line0, line1) +{ + var dx0 = line0.getDeltaX(); + var dy0 = line0.getDeltaY(); + var dx1 = line1.getDeltaX(); + var dy1 = line1.getDeltaY(); + // dot = ||a||*||b||*cos(theta) + var dot = dx0 * dx1 + dy0 * dy1; + // cross = ||a||*||b||*sin(theta) + var det = dx0 * dy1 - dy0 * dx1; + // tan = sin / cos + var angle = Math.atan2( det, dot ) * 180 / Math.PI; + // complementary angle + // shift? + return 360 - (180 - angle); +}; + +/** + * Get a perpendicular line to an input one. + * @param {Object} line The line to be perpendicular to. + * @param {Object} point The middle point of the perpendicular line. + * @param {Number} length The length of the perpendicular line. + */ +dwv.math.getPerpendicularLine = function (line, point, length) +{ + // begin point + var beginX = 0; + var beginY = 0; + // end point + var endX = 0; + var endY = 0; + + // check slope: + // 0 -> horizontal + // Infinite -> vertical (a/Infinite = 0) + if ( line.getSlope() !== 0 ) { + // a0 * a1 = -1 + var slope = -1 / line.getSlope(); + // y0 = a1*x0 + b1 -> b1 = y0 - a1*x0 + var intercept = point.getY() - slope * point.getX(); + + // 1. (x - x0)^2 + (y - y0)^2 = d^2 + // 2. a = (y - y0) / (x - x0) -> y = a*(x - x0) + y0 + // -> (x - x0)^2 + m^2 * (x - x0)^2 = d^2 + // -> x = x0 +- d / sqrt(1+m^2) + + // length is the distance between begin and end, + // point is half way between both -> d = length / 2 + var dx = length / ( 2 * Math.sqrt( 1 + slope * slope ) ); + + // begin point + beginX = point.getX() - dx; + beginY = slope * beginX + intercept; + // end point + endX = point.getX() + dx; + endY = slope * endX + intercept; + } + else { + // horizontal input line -> perpendicular is vertical! + // begin point + beginX = point.getX(); + beginY = point.getY() - length / 2; + // end point + endX = point.getX(); + endY = point.getY() + length / 2; + } + // perpendicalar line + return new dwv.math.Line( + new dwv.math.Point2D(beginX, beginY), + new dwv.math.Point2D(endX, endY) ); +}; + +/** + * Rectangle shape. + * @constructor + * @param {Object} begin A Point2D representing the beginning of the rectangle. + * @param {Object} end A Point2D representing the end of the rectangle. + */ +dwv.math.Rectangle = function(begin, end) +{ + if ( end.getX() < begin.getX() ) { + var tmpX = begin.getX(); + begin = new dwv.math.Point2D( end.getX(), begin.getY() ); + end = new dwv.math.Point2D( tmpX, end.getY() ); + } + if ( end.getY() < begin.getY() ) { + var tmpY = begin.getY(); + begin = new dwv.math.Point2D( begin.getX(), end.getY() ); + end = new dwv.math.Point2D( end.getX(), tmpY ); + } + + /** + * Rectangle surface. + * @private + * @type Number + */ + var surface = Math.abs(end.getX() - begin.getX()) * Math.abs(end.getY() - begin.getY() ); + + /** + * Get the begin point of the rectangle. + * @return {Object} The begin point of the rectangle + */ + this.getBegin = function() { return begin; }; + /** + * Get the end point of the rectangle. + * @return {Object} The end point of the rectangle + */ + this.getEnd = function() { return end; }; + /** + * Get the real width of the rectangle. + * @return {Number} The real width of the rectangle. + */ + this.getRealWidth = function() { return end.getX() - begin.getX(); }; + /** + * Get the real height of the rectangle. + * @return {Number} The real height of the rectangle. + */ + this.getRealHeight = function() { return end.getY() - begin.getY(); }; + /** + * Get the width of the rectangle. + * @return {Number} The width of the rectangle. + */ + this.getWidth = function() { return Math.abs(this.getRealWidth()); }; + /** + * Get the height of the rectangle. + * @return {Number} The height of the rectangle. + */ + this.getHeight = function() { return Math.abs(this.getRealHeight()); }; + /** + * Get the surface of the rectangle. + * @return {Number} The surface of the rectangle. + */ + this.getSurface = function() { return surface; }; + /** + * Get the surface of the circle according to a spacing. + * @param {Number} spacingX The X spacing. + * @param {Number} spacingY The Y spacing. + * @return {Number} The surface of the rectangle multiplied by the given + * spacing or null for null spacings. + */ + this.getWorldSurface = function(spacingX, spacingY) + { + return mulABC(surface, spacingX, spacingY); + }; +}; // Rectangle class + +/** + * Region Of Interest shape. + * Note: should be a closed path. + * @constructor + */ +dwv.math.ROI = function() +{ + /** + * List of points. + * @private + * @type Array + */ + var points = []; + + /** + * Get a point of the list at a given index. + * @param {Number} index The index of the point to get (beware, no size check). + * @return {Object} The Point2D at the given index. + */ + this.getPoint = function(index) { return points[index]; }; + /** + * Get the length of the point list. + * @return {Number} The length of the point list. + */ + this.getLength = function() { return points.length; }; + /** + * Add a point to the ROI. + * @param {Object} point The Point2D to add. + */ + this.addPoint = function(point) { points.push(point); }; + /** + * Add points to the ROI. + * @param {Array} rhs The array of POints2D to add. + */ + this.addPoints = function(rhs) { points=points.concat(rhs);}; +}; // ROI class + +/** + * Path shape. + * @constructor + * @param {Array} inputPointArray The list of Point2D that make the path (optional). + * @param {Array} inputControlPointIndexArray The list of control point of path, + * as indexes (optional). + * Note: first and last point do not need to be equal. + */ +dwv.math.Path = function(inputPointArray, inputControlPointIndexArray) +{ + /** + * List of points. + * @type Array + */ + this.pointArray = inputPointArray ? inputPointArray.slice() : []; + /** + * List of control points. + * @type Array + */ + this.controlPointIndexArray = inputControlPointIndexArray ? + inputControlPointIndexArray.slice() : []; +}; // Path class + +/** + * Get a point of the list. + * @param {Number} index The index of the point to get (beware, no size check). + * @return {Object} The Point2D at the given index. + */ +dwv.math.Path.prototype.getPoint = function(index) { + return this.pointArray[index]; +}; + +/** + * Is the given point a control point. + * @param {Object} point The Point2D to check. + * @return {Boolean} True if a control point. + */ +dwv.math.Path.prototype.isControlPoint = function(point) { + var index = this.pointArray.indexOf(point); + if( index !== -1 ) { + return this.controlPointIndexArray.indexOf(index) !== -1; + } + else { + throw new Error("Error: isControlPoint called with not in list point."); + } +}; + +/** + * Get the length of the path. + * @return {Number} The length of the path. + */ +dwv.math.Path.prototype.getLength = function() { + return this.pointArray.length; +}; + +/** + * Add a point to the path. + * @param {Object} point The Point2D to add. + */ +dwv.math.Path.prototype.addPoint = function(point) { + this.pointArray.push(point); +}; + +/** + * Add a control point to the path. + * @param {Object} point The Point2D to make a control point. + */ +dwv.math.Path.prototype.addControlPoint = function(point) { + var index = this.pointArray.indexOf(point); + if( index !== -1 ) { + this.controlPointIndexArray.push(index); + } + else { + throw new Error("Error: addControlPoint called with no point in list point."); + } +}; + +/** + * Add points to the path. + * @param {Array} points The list of Point2D to add. + */ +dwv.math.Path.prototype.addPoints = function(newPointArray) { + this.pointArray = this.pointArray.concat(newPointArray); +}; + +/** + * Append a Path to this one. + * @param {Path} other The Path to append. + */ +dwv.math.Path.prototype.appenPath = function(other) { + var oldSize = this.pointArray.length; + this.pointArray = this.pointArray.concat(other.pointArray); + var indexArray = []; + for( var i=0; i < other.controlPointIndexArray.length; ++i ) { + indexArray[i] = other.controlPointIndexArray[i] + oldSize; + } + this.controlPointIndexArray = this.controlPointIndexArray.concat(indexArray); +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Get the minimum, maximum, mean and standard deviation + * of an array of values. + * Note: could use {@link https://github.com/tmcw/simple-statistics}. + */ +dwv.math.getStats = function (array) +{ + var min = array[0]; + var max = min; + var mean = 0; + var sum = 0; + var sumSqr = 0; + var stdDev = 0; + var variance = 0; + + var val = 0; + for ( var i = 0; i < array.length; ++i ) { + val = array[i]; + if ( val < min ) { + min = val; + } + else if ( val > max ) { + max = val; + } + sum += val; + sumSqr += val * val; + } + + mean = sum / array.length; + // see http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance + variance = sumSqr / array.length - mean * mean; + stdDev = Math.sqrt(variance); + + return { 'min': min, 'max': max, 'mean': mean, 'stdDev': stdDev }; +}; + +/** + * Unique ID generator. + * See {@link http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript} + * and this {@link http://stackoverflow.com/a/13403498 answer}. + */ +dwv.math.guid = function () +{ + return Math.random().toString(36).substring(2, 15); +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Immutable 3D vector. + * @constructor + * @param {Number} x The X component of the vector. + * @param {Number} y The Y component of the vector. + * @param {Number} z The Z component of the vector. + */ +dwv.math.Vector3D = function (x,y,z) +{ + /** + * Get the X component of the vector. + * @return {Number} The X component of the vector. + */ + this.getX = function () { return x; }; + /** + * Get the Y component of the vector. + * @return {Number} The Y component of the vector. + */ + this.getY = function () { return y; }; + /** + * Get the Z component of the vector. + * @return {Number} The Z component of the vector. + */ + this.getZ = function () { return z; }; +}; // Vector3D class + +/** + * Check for Vector3D equality. + * @param {Object} rhs The other vector to compare to. + * @return {Boolean} True if both vectors are equal. + */ +dwv.math.Vector3D.prototype.equals = function (rhs) { + return rhs !== null && + this.getX() === rhs.getX() && + this.getY() === rhs.getY() && + this.getZ() === rhs.getZ(); +}; + +/** + * Get a string representation of the Vector3D. + * @return {String} The vector as a string. + */ +dwv.math.Vector3D.prototype.toString = function () { + return "(" + this.getX() + + ", " + this.getY() + + ", " + this.getZ() + ")"; +}; + +/** + * Get the norm of the vector. + * @return {Number} The norm. + */ +dwv.math.Vector3D.prototype.norm = function () { + return Math.sqrt( (this.getX() * this.getX()) + + (this.getY() * this.getY()) + + (this.getZ() * this.getZ()) ); +}; + +/** + * Get the cross product with another Vector3D, ie the + * vector that is perpendicular to both a and b. + * If both vectors are parallel, the cross product is a zero vector. + * @param {Object} vector3D The input vector. + * @return {Object} The result vector. + */ +dwv.math.Vector3D.prototype.crossProduct = function (vector3D) { + return new dwv.math.Vector3D( + (this.getY() * vector3D.getZ()) - (vector3D.getY() * this.getZ()), + (this.getZ() * vector3D.getX()) - (vector3D.getZ() * this.getX()), + (this.getX() * vector3D.getY()) - (vector3D.getX() * this.getY()) ); +}; + +/** + * Get the dot product with another Vector3D. + * @param {Object} vector3D The input vector. + * @return {Number} The dot product. + */ +dwv.math.Vector3D.prototype.dotProduct = function (vector3D) { + return (this.getX() * vector3D.getX()) + + (this.getY() * vector3D.getY()) + + (this.getZ() * vector3D.getZ()); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Arrow factory. + * @constructor + * @external Konva + */ +dwv.tool.ArrowFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 2; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 0; }; +}; + +/** + * Create an arrow shape to be displayed. + * @param {Array} points The points from which to extract the line. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.ArrowFactory.prototype.create = function (points, style/*, image*/) +{ + // physical shape + var line = new dwv.math.Line(points[0], points[1]); + // draw shape + var kshape = new Konva.Line({ + points: [line.getBegin().getX(), line.getBegin().getY(), + line.getEnd().getX(), line.getEnd().getY() ], + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape" + }); + // larger hitfunc + var linePerp0 = dwv.math.getPerpendicularLine( line, points[0], 10 ); + var linePerp1 = dwv.math.getPerpendicularLine( line, points[1], 10 ); + kshape.hitFunc( function (context) { + context.beginPath(); + context.moveTo( linePerp0.getBegin().getX(), linePerp0.getBegin().getY() ); + context.lineTo( linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ); + context.lineTo( linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ); + context.lineTo( linePerp1.getBegin().getX(), linePerp1.getBegin().getY() ); + context.closePath(); + context.fillStrokeShape(this); + }); + // triangle + var beginTy = new dwv.math.Point2D(line.getBegin().getX(), line.getBegin().getY() - 10); + var verticalLine = new dwv.math.Line(line.getBegin(), beginTy); + var angle = dwv.math.getAngle(line, verticalLine); + var angleRad = angle * Math.PI / 180; + var radius = 5; + var kpoly = new Konva.RegularPolygon({ + x: line.getBegin().getX() + radius * Math.sin(angleRad), + y: line.getBegin().getY() + radius * Math.cos(angleRad), + sides: 3, + radius: radius, + rotation: -angle, + fill: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape-triangle" + }); + // quantification + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = ""; + ktext.longText = ""; + ktext.quant = null; + ktext.setText(ktext.textExpr); + // label + var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1; + var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5; + var klabel = new Konva.Label({ + x: line.getEnd().getX() + dX * 25, + y: line.getEnd().getY() + dY * 15, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("line-group"); + group.add(kshape); + group.add(kpoly); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update an arrow shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateArrow = function (anchor/*, image*/) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kline = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated triangle shape + var ktriangle = group.getChildren( function (node) { + return node.name() === 'shape-triangle'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + // find special points + var begin = group.getChildren( function (node) { + return node.id() === 'begin'; + })[0]; + var end = group.getChildren( function (node) { + return node.id() === 'end'; + })[0]; + // update special points + switch ( anchor.id() ) { + case 'begin': + begin.x( anchor.x() ); + begin.y( anchor.y() ); + break; + case 'end': + end.x( anchor.x() ); + end.y( anchor.y() ); + break; + } + // update shape and compensate for possible drag + // note: shape.position() and shape.size() won't work... + var bx = begin.x() - kline.x(); + var by = begin.y() - kline.y(); + var ex = end.x() - kline.x(); + var ey = end.y() - kline.y(); + kline.points( [bx,by,ex,ey] ); + // new line + var p2d0 = new dwv.math.Point2D(begin.x(), begin.y()); + var p2d1 = new dwv.math.Point2D(end.x(), end.y()); + var line = new dwv.math.Line(p2d0, p2d1); + // larger hitfunc + var p2b = new dwv.math.Point2D(bx, by); + var p2e = new dwv.math.Point2D(ex, ey); + var linePerp0 = dwv.math.getPerpendicularLine( line, p2b, 10 ); + var linePerp1 = dwv.math.getPerpendicularLine( line, p2e, 10 ); + kline.hitFunc( function (context) { + context.beginPath(); + context.moveTo( linePerp0.getBegin().getX(), linePerp0.getBegin().getY() ); + context.lineTo( linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ); + context.lineTo( linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ); + context.lineTo( linePerp1.getBegin().getX(), linePerp1.getBegin().getY() ); + context.closePath(); + context.fillStrokeShape(this); + }); + // udate triangle + var beginTy = new dwv.math.Point2D(line.getBegin().getX(), line.getBegin().getY() - 10); + var verticalLine = new dwv.math.Line(line.getBegin(), beginTy); + var angle = dwv.math.getAngle(line, verticalLine); + var angleRad = angle * Math.PI / 180; + ktriangle.x(line.getBegin().getX() + ktriangle.radius() * Math.sin(angleRad)); + ktriangle.y(line.getBegin().getY() + ktriangle.radius() * Math.cos(angleRad)); + ktriangle.rotation(-angle); + // update text + var ktext = klabel.getText(); + ktext.quant = null; + ktext.setText(ktext.textExpr); + // update position + var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1; + var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5; + var textPos = { + 'x': line.getEnd().getX() + dX * 25, + 'y': line.getEnd().getY() + dY * 15 }; + klabel.position( textPos ); +}; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Drawing tool. + * + * This tool is responsible for the draw layer group structure. The layout is: + * + * drawLayer + * |_ positionGroup: name="position-group", id="slice-#_frame-#"" + * |_ shapeGroup: name="{shape name}-group", id="#" + * |_ shape: name="shape" + * |_ label: name="label" + * |_ extra: line tick, protractor arc... + * + * Discussion: + * - posGroup > shapeGroup + * pro: slice/frame display: 1 loop + * cons: multi-slice shape splitted in positionGroups + * - shapeGroup > posGroup + * pros: more logical + * cons: slice/frame display: 2 loops + * + * @constructor + * @param {Object} app The associated application. + * @external Konva + */ +dwv.tool.Draw = function (app, shapeFactoryList) +{ + /** + * Closure to self: to be used by event handlers. + * @private + * @type WindowLevel + */ + var self = this; + /** + * Draw GUI. + * @type Object + */ + var gui = null; + /** + * Interaction start flag. + * @private + * @type Boolean + */ + var started = false; + + /** + * Shape factory list + * @type Object + */ + this.shapeFactoryList = shapeFactoryList; + /** + * Draw command. + * @private + * @type Object + */ + var command = null; + /** + * Current shape group. + * @private + * @type Object + */ + var tmpShapeGroup = null; + + /** + * Shape name. + * @type String + */ + this.shapeName = 0; + + /** + * List of points. + * @private + * @type Array + */ + var points = []; + + /** + * Last selected point. + * @private + * @type Object + */ + var lastPoint = null; + + /** + * Shape editor. + * @private + * @type Object + */ + var shapeEditor = new dwv.tool.ShapeEditor(app); + + // associate the event listeners of the editor + // with those of the draw tool + shapeEditor.setDrawEventCallback(fireEvent); + + /** + * Trash draw: a cross. + * @private + * @type Object + */ + var trash = new Konva.Group(); + + // first line of the cross + var trashLine1 = new Konva.Line({ + points: [-10, -10, 10, 10 ], + stroke: 'red' + }); + // second line of the cross + var trashLine2 = new Konva.Line({ + points: [10, -10, -10, 10 ], + stroke: 'red' + }); + trash.add(trashLine1); + trash.add(trashLine2); + + /** + * Drawing style. + * @type Style + */ + this.style = new dwv.html.Style(); + + /** + * Event listeners. + * @private + */ + var listeners = {}; + + /** + * The associated draw layer. + * @private + * @type Object + */ + var drawLayer = null; + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + // determine if the click happened in an existing shape + var stage = app.getDrawStage(); + var kshape = stage.getIntersection({ + x: event._xs, + y: event._ys + }); + + if ( kshape ) { + var group = kshape.getParent(); + var selectedShape = group.find(".shape")[0]; + // reset editor if click on other shape + // (and avoid anchors mouse down) + if ( selectedShape && selectedShape !== shapeEditor.getShape() ) { + shapeEditor.disable(); + shapeEditor.setShape(selectedShape); + shapeEditor.setImage(app.getImage()); + shapeEditor.enable(); + } + } + else { + // disable edition + shapeEditor.disable(); + shapeEditor.setShape(null); + shapeEditor.setImage(null); + // start storing points + started = true; + // clear array + points = []; + // store point + lastPoint = new dwv.math.Point2D(event._x, event._y); + points.push(lastPoint); + } + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + if (!started) + { + return; + } + if ( Math.abs( event._x - lastPoint.getX() ) > 0 || + Math.abs( event._y - lastPoint.getY() ) > 0 ) + { + // current point + lastPoint = new dwv.math.Point2D(event._x, event._y); + // clear last added point from the list (but not the first one) + if ( points.length != 1 ) { + points.pop(); + } + // add current one to the list + points.push( lastPoint ); + // allow for anchor points + var factory = new self.shapeFactoryList[self.shapeName](); + if( points.length < factory.getNPoints() ) { + clearTimeout(this.timer); + this.timer = setTimeout( function () { + points.push( lastPoint ); + }, factory.getTimeout() ); + } + // remove previous draw + if ( tmpShapeGroup ) { + tmpShapeGroup.destroy(); + } + // create shape group + tmpShapeGroup = factory.create(points, self.style, app.getImage()); + // do not listen during creation + var shape = tmpShapeGroup.getChildren( dwv.draw.isNodeNameShape )[0]; + shape.listening(false); + drawLayer.hitGraphEnabled(false); + // draw shape + drawLayer.add(tmpShapeGroup); + drawLayer.draw(); + } + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function (/*event*/){ + if (started && points.length > 1 ) + { + // reset shape group + if ( tmpShapeGroup ) { + tmpShapeGroup.destroy(); + } + // create final shape + var factory = new self.shapeFactoryList[self.shapeName](); + var finalShapeGroup = factory.create(points, self.style, app.getImage()); + finalShapeGroup.id( dwv.math.guid() ); + + // get the position group + var posGroup = app.getDrawController().getCurrentPosGroup(); + // add shape group to position group + posGroup.add(finalShapeGroup); + + // re-activate layer + drawLayer.hitGraphEnabled(true); + // draw shape command + command = new dwv.tool.DrawGroupCommand(finalShapeGroup, self.shapeName, drawLayer); + command.onExecute = fireEvent; + command.onUndo = fireEvent; + // execute it + command.execute(); + // save it in undo stack + app.addToUndoStack(command); + + // activate shape listeners + self.setShapeOn( finalShapeGroup ); + } + // reset flag + started = false; + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(event){ + self.mouseup(event); + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + self.mousedown(event); + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + self.mousemove(event); + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(event){ + self.mouseup(event); + }; + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + app.onKeydown(event); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.Draw(app); + gui.setup(this.shapeFactoryList); + }; + + /** + * Enable the tool. + * @param {Boolean} flag The flag to enable or not. + */ + this.display = function ( flag ){ + if ( gui ) { + gui.display( flag ); + } + // reset shape display properties + shapeEditor.disable(); + shapeEditor.setShape(null); + shapeEditor.setImage(null); + document.body.style.cursor = 'default'; + // make layer listen or not to events + app.getDrawStage().listening( flag ); + // get the current draw layer + drawLayer = app.getDrawController().getDrawLayer(); + renderDrawLayer(flag); + // listen to app change to update the draw layer + if (flag) { + app.addEventListener("slice-change", updateDrawLayer); + app.addEventListener("frame-change", updateDrawLayer); + } + else { + app.removeEventListener("slice-change", updateDrawLayer); + app.removeEventListener("frame-change", updateDrawLayer); + } + }; + + /** + * Update the draw layer. + */ + function updateDrawLayer() { + // activate the draw layer + renderDrawLayer(true); + } + + /** + * Render (or not) the draw layer. + * @param {Boolean} visible Set the draw layer visible or not. + */ + function renderDrawLayer(visible) { + + drawLayer.listening( visible ); + drawLayer.hitGraphEnabled( visible ); + + // get shape groups at the current position + var shapeGroups = app.getDrawController().getCurrentPosGroup().getChildren(); + + // set shape display properties + if ( visible ) { + // activate tool listeners + app.addToolCanvasListeners( app.getDrawStage().getContent() ); + // activate shape listeners + shapeGroups.forEach( function (group) { self.setShapeOn( group ); }); + } + else { + // de-activate tool listeners + app.removeToolCanvasListeners( app.getDrawStage().getContent() ); + // de-activate shape listeners + shapeGroups.forEach( function (group) { setShapeOff( group ); }); + } + // draw + drawLayer.draw(); + } + + /** + * Set shape group off properties. + * @param {Object} shapeGroup The shape group to set off. + */ + function setShapeOff( shapeGroup ) { + // mouse styling + shapeGroup.off('mouseover'); + shapeGroup.off('mouseout'); + // drag + shapeGroup.draggable(false); + shapeGroup.off('dragstart.draw'); + shapeGroup.off('dragmove.draw'); + shapeGroup.off('dragend.draw'); + shapeGroup.off('dblclick'); + } + + /** + * Get the real position from an event. + */ + function getRealPosition( index ) { + var stage = app.getDrawStage(); + return { 'x': stage.offset().x + index.x / stage.scale().x, + 'y': stage.offset().y + index.y / stage.scale().y }; + } + + /** + * Set shape group on properties. + * @param {Object} shapeGroup The shape group to set on. + */ + this.setShapeOn = function ( shapeGroup ) { + // mouse over styling + shapeGroup.on('mouseover', function () { + document.body.style.cursor = 'pointer'; + }); + // mouse out styling + shapeGroup.on('mouseout', function () { + document.body.style.cursor = 'default'; + }); + + // make it draggable + shapeGroup.draggable(true); + // cache drag start position + var dragStartPos = {'x': shapeGroup.x(), 'y': shapeGroup.y()}; + + // command name based on shape type + var shapeDisplayName = dwv.tool.GetShapeDisplayName( + shapeGroup.getChildren( dwv.draw.isNodeNameShape )[0]); + + var colour = null; + + // drag start event handling + shapeGroup.on('dragstart.draw', function (/*event*/) { + // store colour + colour = shapeGroup.getChildren( dwv.draw.isNodeNameShape )[0].stroke(); + // display trash + var stage = app.getDrawStage(); + var scale = stage.scale(); + var invscale = {'x': 1/scale.x, 'y': 1/scale.y}; + trash.x( stage.offset().x + ( 256 / scale.x ) ); + trash.y( stage.offset().y + ( 20 / scale.y ) ); + trash.scale( invscale ); + drawLayer.add( trash ); + // deactivate anchors to avoid events on null shape + shapeEditor.setAnchorsActive(false); + // draw + drawLayer.draw(); + }); + // drag move event handling + shapeGroup.on('dragmove.draw', function (event) { + // highlight trash when on it + var offset = dwv.html.getEventOffset( event.evt )[0]; + var eventPos = getRealPosition( offset ); + if ( Math.abs( eventPos.x - trash.x() ) < 10 && + Math.abs( eventPos.y - trash.y() ) < 10 ) { + trash.getChildren().each( function (tshape){ tshape.stroke('orange'); }); + // change the group shapes colour + shapeGroup.getChildren( dwv.draw.canNodeChangeColour ).forEach( + function (ashape) { ashape.stroke( 'red' ); }); + } + else { + trash.getChildren().each( function (tshape){ tshape.stroke('red'); }); + // reset the group shapes colour + shapeGroup.getChildren( dwv.draw.canNodeChangeColour ).forEach( + function (ashape) { ashape.stroke( colour ); }); + } + // draw + drawLayer.draw(); + }); + // drag end event handling + shapeGroup.on('dragend.draw', function (event) { + var pos = {'x': this.x(), 'y': this.y()}; + // remove trash + trash.remove(); + // delete case + var offset = dwv.html.getEventOffset( event.evt )[0]; + var eventPos = getRealPosition( offset ); + if ( Math.abs( eventPos.x - trash.x() ) < 10 && + Math.abs( eventPos.y - trash.y() ) < 10 ) { + // compensate for the drag translation + this.x( dragStartPos.x ); + this.y( dragStartPos.y ); + // disable editor + shapeEditor.disable(); + shapeEditor.setShape(null); + shapeEditor.setImage(null); + // reset colour + shapeGroup.getChildren( dwv.draw.canNodeChangeColour ).forEach( + function (ashape) { ashape.stroke( colour ); }); + // reset cursor + document.body.style.cursor = 'default'; + // delete command + var delcmd = new dwv.tool.DeleteGroupCommand(this, + shapeDisplayName, drawLayer); + delcmd.onExecute = fireEvent; + delcmd.onUndo = fireEvent; + delcmd.execute(); + app.addToUndoStack(delcmd); + } + else { + // save drag move + var translation = {'x': pos.x - dragStartPos.x, + 'y': pos.y - dragStartPos.y}; + if ( translation.x !== 0 || translation.y !== 0 ) { + var mvcmd = new dwv.tool.MoveGroupCommand(this, + shapeDisplayName, translation, drawLayer); + mvcmd.onExecute = fireEvent; + mvcmd.onUndo = fireEvent; + app.addToUndoStack(mvcmd); + + // the move is handled by Konva, trigger an event manually + fireEvent({'type': 'draw-move'}); + } + // reset anchors + shapeEditor.setAnchorsActive(true); + shapeEditor.resetAnchors(); + } + // draw + drawLayer.draw(); + // reset start position + dragStartPos = {'x': this.x(), 'y': this.y()}; + }); + // double click handling: update label + shapeGroup.on('dblclick', function () { + + // get the label object for this shape + var label = this.findOne('Label'); + // should just be one + if ( typeof label === "undefined" ) { + throw new Error("Could not find the shape label."); + } + var ktext = label.getText(); + + // ask user for new label + var labelText = dwv.gui.prompt("Shape label", ktext.textExpr); + + // if press cancel do nothing + if (labelText === null) { + return; + } + else if (labelText === ktext.textExpr) { + return; + } + // update text expression and set text + ktext.textExpr = labelText; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + + // trigger event + fireEvent({'type': 'draw-change'}); + + // draw + drawLayer.draw(); + }); + }; + + /** + * Initialise the tool. + */ + this.init = function() { + // set the default to the first in the list + var shapeName = 0; + for( var key in this.shapeFactoryList ){ + shapeName = key; + break; + } + this.setShapeName(shapeName); + // init gui + if ( gui ) { + // init with the app window scale + this.style.setScale(app.getWindowScale()); + // same for colour + this.setLineColour(this.style.getLineColour()); + // init html + gui.initialise(); + } + return true; + }; + + /** + * Add an event listener on the app. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ + this.addEventListener = function (type, listener) + { + if ( typeof listeners[type] === "undefined" ) { + listeners[type] = []; + } + listeners[type].push(listener); + }; + + /** + * Remove an event listener from the app. + * @param {String} type The event type. + * @param {Object} listener The method associated with the provided event type. + */ + this.removeEventListener = function (type, listener) + { + if( typeof listeners[type] === "undefined" ) { + return; + } + for ( var i = 0; i < listeners[type].length; ++i ) + { + if ( listeners[type][i] === listener ) { + listeners[type].splice(i,1); + } + } + }; + + /** + * Set the line colour of the drawing. + * @param {String} colour The colour to set. + */ + this.setLineColour = function (colour) + { + this.style.setLineColour(colour); + }; + + // Private Methods ----------------------------------------------------------- + + /** + * Fire an event: call all associated listeners. + * @param {Object} event The event to fire. + */ + function fireEvent (event) + { + if ( typeof listeners[event.type] === "undefined" ) { + return; + } + for ( var i=0; i < listeners[event.type].length; ++i ) + { + listeners[event.type][i](event); + } + } + +}; // Draw class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.Draw.prototype.getHelp = function() +{ + return { + "title": dwv.i18n("tool.Draw.name"), + "brief": dwv.i18n("tool.Draw.brief"), + "mouse": { + "mouse_drag": dwv.i18n("tool.Draw.mouse_drag") + }, + "touch": { + "touch_drag": dwv.i18n("tool.Draw.touch_drag") + } + }; +}; + +/** + * Set the shape name of the drawing. + * @param {String} name The name of the shape. + */ +dwv.tool.Draw.prototype.setShapeName = function(name) +{ + // check if we have it + if( !this.hasShape(name) ) + { + throw new Error("Unknown shape: '" + name + "'"); + } + this.shapeName = name; +}; + +/** + * Check if the shape is in the shape list. + * @param {String} name The name of the shape. + */ +dwv.tool.Draw.prototype.hasShape = function(name) { + return this.shapeFactoryList[name]; +}; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Get the display name of the input shape. + * @param {Object} shape The Konva shape. + * @return {String} The display name. + * @external Konva + */ +dwv.tool.GetShapeDisplayName = function (shape) +{ + var displayName = "shape"; + if ( shape instanceof Konva.Line ) { + if ( shape.points().length === 4 ) { + displayName = "line"; + } + else if ( shape.points().length === 6 ) { + displayName = "protractor"; + } + else { + displayName = "roi"; + } + } + else if ( shape instanceof Konva.Rect ) { + displayName = "rectangle"; + } + else if ( shape instanceof Konva.Ellipse ) { + displayName = "ellipse"; + } + // return + return displayName; +}; + +/** + * Draw group command. + * @param {Object} group The group draw. + * @param {String} name The shape display name. + * @param {Object} layer The layer where to draw the group. + * @param {Object} silent Whether to send a creation event or not. + * @constructor + */ +dwv.tool.DrawGroupCommand = function (group, name, layer, silent) +{ + var isSilent = (typeof silent === "undefined") ? false : true; + + // group parent + var parent = group.getParent(); + + /** + * Get the command name. + * @return {String} The command name. + */ + this.getName = function () { return "Draw-"+name; }; + /** + * Execute the command. + */ + this.execute = function () { + // add the group to the parent (in case of undo/redo) + parent.add(group); + // draw + layer.draw(); + // callback + if (!isSilent) { + this.onExecute({'type': 'draw-create', 'id': group.id()}); + } + }; + /** + * Undo the command. + */ + this.undo = function () { + // remove the group from the parent layer + group.remove(); + // draw + layer.draw(); + // callback + this.onUndo({'type': 'draw-delete', 'id': group.id()}); + }; +}; // DrawGroupCommand class + +/** + * Handle an execute event. + * @param {Object} event The execute event with type and id. + */ +dwv.tool.DrawGroupCommand.prototype.onExecute = function (/*event*/) +{ + // default does nothing. +}; +/** + * Handle an undo event. + * @param {Object} event The undo event with type and id. + */ +dwv.tool.DrawGroupCommand.prototype.onUndo = function (/*event*/) +{ + // default does nothing. +}; + +/** + * Move group command. + * @param {Object} group The group draw. + * @param {String} name The shape display name. + * @param {Object} translation A 2D translation to move the group by. + * @param {Object} layer The layer where to move the group. + * @constructor + */ +dwv.tool.MoveGroupCommand = function (group, name, translation, layer) +{ + /** + * Get the command name. + * @return {String} The command name. + */ + this.getName = function () { return "Move-"+name; }; + + /** + * Execute the command. + */ + this.execute = function () { + // translate group + group.move(translation); + // draw + layer.draw(); + // callback + this.onExecute({'type': 'draw-move', 'id': group.id()}); + }; + /** + * Undo the command. + */ + this.undo = function () { + // invert translate group + var minusTrans = { 'x': -translation.x, 'y': -translation.y}; + group.move(minusTrans); + // draw + layer.draw(); + // callback + this.onUndo({'type': 'draw-move', 'id': group.id()}); + }; +}; // MoveGroupCommand class + +/** + * Handle an execute event. + * @param {Object} event The execute event with type and id. + */ +dwv.tool.MoveGroupCommand.prototype.onExecute = function (/*event*/) +{ + // default does nothing. +}; +/** + * Handle an undo event. + * @param {Object} event The undo event with type and id. + */ +dwv.tool.MoveGroupCommand.prototype.onUndo = function (/*event*/) +{ + // default does nothing. +}; + +/** + * Change group command. + * @param {String} name The shape display name. + * @param {Object} func The change function. + * @param {Object} startAnchor The anchor that starts the change. + * @param {Object} endAnchor The anchor that ends the change. + * @param {Object} layer The layer where to change the group. + * @param {Object} image The associated image. + * @constructor + */ +dwv.tool.ChangeGroupCommand = function (name, func, startAnchor, endAnchor, layer, image) +{ + /** + * Get the command name. + * @return {String} The command name. + */ + this.getName = function () { return "Change-"+name; }; + + /** + * Execute the command. + */ + this.execute = function () { + // change shape + func( endAnchor, image ); + // draw + layer.draw(); + // callback + this.onExecute({'type': 'draw-change'}); + }; + /** + * Undo the command. + */ + this.undo = function () { + // invert change shape + func( startAnchor, image ); + // draw + layer.draw(); + // callback + this.onUndo({'type': 'draw-change'}); + }; +}; // ChangeGroupCommand class + +/** + * Handle an execute event. + * @param {Object} event The execute event with type and id. + */ +dwv.tool.ChangeGroupCommand.prototype.onExecute = function (/*event*/) +{ + // default does nothing. +}; +/** + * Handle an undo event. + * @param {Object} event The undo event with type and id. + */ +dwv.tool.ChangeGroupCommand.prototype.onUndo = function (/*event*/) +{ + // default does nothing. +}; + +/** + * Delete group command. + * @param {Object} group The group draw. + * @param {String} name The shape display name. + * @param {Object} layer The layer where to delete the group. + * @constructor + */ +dwv.tool.DeleteGroupCommand = function (group, name, layer) +{ + // group parent + var parent = group.getParent(); + + /** + * Get the command name. + * @return {String} The command name. + */ + this.getName = function () { return "Delete-"+name; }; + /** + * Execute the command. + */ + this.execute = function () { + // remove the group from its parent + group.remove(); + // draw + layer.draw(); + // callback + this.onExecute({'type': 'draw-delete', 'id': group.id()}); + }; + /** + * Undo the command. + */ + this.undo = function () { + // add the group to its parent + parent.add(group); + // draw + layer.draw(); + // callback + this.onUndo({'type': 'draw-create', 'id': group.id()}); + }; +}; // DeleteGroupCommand class + +/** + * Handle an execute event. + * @param {Object} event The execute event with type and id. + */ +dwv.tool.DeleteGroupCommand.prototype.onExecute = function (/*event*/) +{ + // default does nothing. +}; +/** + * Handle an undo event. + * @param {Object} event The undo event with type and id. + */ +dwv.tool.DeleteGroupCommand.prototype.onUndo = function (/*event*/) +{ + // default does nothing. +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Shape editor. + * @constructor + * @external Konva + */ +dwv.tool.ShapeEditor = function (app) +{ + /** + * Edited shape. + * @private + * @type Object + */ + var shape = null; + /** + * Edited image. Used for quantification update. + * @private + * @type Object + */ + var image = null; + /** + * Active flag. + * @private + * @type Boolean + */ + var isActive = false; + /** + * Update function used by anchors to update the shape. + * @private + * @type Function + */ + var updateFunction = null; + /** + * Draw event callback. + * @private + * @type Function + */ + var drawEventCallback = null; + + /** + * Set the shape to edit. + * @param {Object} inshape The shape to edit. + */ + this.setShape = function ( inshape ) { + shape = inshape; + // reset anchors + if ( shape ) { + removeAnchors(); + addAnchors(); + } + }; + + /** + * Set the associated image. + * @param {Object} img The associated image. + */ + this.setImage = function ( img ) { + image = img; + }; + + /** + * Get the edited shape. + * @return {Object} The edited shape. + */ + this.getShape = function () { + return shape; + }; + + /** + * Get the active flag. + * @return {Boolean} The active flag. + */ + this.isActive = function () { + return isActive; + }; + + /** + * Set the draw event callback. + * @param {Object} callback The callback. + */ + this.setDrawEventCallback = function ( callback ) { + drawEventCallback = callback; + }; + + /** + * Enable the editor. Redraws the layer. + */ + this.enable = function () { + isActive = true; + if ( shape ) { + setAnchorsVisible( true ); + if ( shape.getLayer() ) { + shape.getLayer().draw(); + } + } + }; + + /** + * Disable the editor. Redraws the layer. + */ + this.disable = function () { + isActive = false; + if ( shape ) { + setAnchorsVisible( false ); + if ( shape.getLayer() ) { + shape.getLayer().draw(); + } + } + }; + + /** + * Reset the anchors. + */ + this.resetAnchors = function () { + // remove previous controls + removeAnchors(); + // add anchors + addAnchors(); + // set them visible + setAnchorsVisible( true ); + }; + + /** + * Apply a function on all anchors. + * @param {Object} func A f(shape) function. + */ + function applyFuncToAnchors( func ) { + if ( shape && shape.getParent() ) { + var anchors = shape.getParent().find('.anchor'); + anchors.each( func ); + } + } + + /** + * Set anchors visibility. + * @param {Boolean} flag The visible flag. + */ + function setAnchorsVisible( flag ) { + applyFuncToAnchors( function (anchor) { + anchor.visible( flag ); + }); + } + + /** + * Set anchors active. + * @param {Boolean} flag The active (on/off) flag. + */ + this.setAnchorsActive = function ( flag ) { + var func = null; + if ( flag ) { + func = function (anchor) { + setAnchorOn( anchor ); + }; + } + else { + func = function (anchor) { + setAnchorOff( anchor ); + }; + } + applyFuncToAnchors( func ); + }; + + /** + * Remove anchors. + */ + function removeAnchors() { + applyFuncToAnchors( function (anchor) { + anchor.remove(); + }); + } + + /** + * Add the shape anchors. + */ + function addAnchors() { + // exit if no shape or no layer + if ( !shape || !shape.getLayer() ) { + return; + } + // get shape group + var group = shape.getParent(); + // add shape specific anchors to the shape group + if ( shape instanceof Konva.Line ) { + var points = shape.points(); + if ( points.length === 4 || points.length === 6) { + // add shape offset + var p0x = points[0] + shape.x(); + var p0y = points[1] + shape.y(); + var p1x = points[2] + shape.x(); + var p1y = points[3] + shape.y(); + addAnchor(group, p0x, p0y, 'begin'); + if ( points.length === 4 ) { + var shapekids = group.getChildren( dwv.draw.isNodeNameShapeExtra ); + if (shapekids.length === 2) { + updateFunction = dwv.tool.UpdateRuler; + } else { + updateFunction = dwv.tool.UpdateArrow; + } + addAnchor(group, p1x, p1y, 'end'); + } + else { + updateFunction = dwv.tool.UpdateProtractor; + addAnchor(group, p1x, p1y, 'mid'); + var p2x = points[4] + shape.x(); + var p2y = points[5] + shape.y(); + addAnchor(group, p2x, p2y, 'end'); + } + } + else { + updateFunction = dwv.tool.UpdateRoi; + var px = 0; + var py = 0; + for ( var i = 0; i < points.length; i=i+2 ) { + px = points[i] + shape.x(); + py = points[i+1] + shape.y(); + addAnchor(group, px, py, i); + } + } + } + else if ( shape instanceof Konva.Rect ) { + updateFunction = dwv.tool.UpdateRect; + var rectX = shape.x(); + var rectY = shape.y(); + var rectWidth = shape.width(); + var rectHeight = shape.height(); + addAnchor(group, rectX, rectY, 'topLeft'); + addAnchor(group, rectX+rectWidth, rectY, 'topRight'); + addAnchor(group, rectX+rectWidth, rectY+rectHeight, 'bottomRight'); + addAnchor(group, rectX, rectY+rectHeight, 'bottomLeft'); + } + else if ( shape instanceof Konva.Ellipse ) { + updateFunction = dwv.tool.UpdateEllipse; + var ellipseX = shape.x(); + var ellipseY = shape.y(); + var radius = shape.radius(); + addAnchor(group, ellipseX-radius.x, ellipseY-radius.y, 'topLeft'); + addAnchor(group, ellipseX+radius.x, ellipseY-radius.y, 'topRight'); + addAnchor(group, ellipseX+radius.x, ellipseY+radius.y, 'bottomRight'); + addAnchor(group, ellipseX-radius.x, ellipseY+radius.y, 'bottomLeft'); + } + // add group to layer + //shape.getLayer().add( group ); + //shape.getParent().add( group ); + } + + /** + * Create shape editor controls, i.e. the anchors. + * @param {Object} group The group associated with this anchor. + * @param {Number} x The X position of the anchor. + * @param {Number} y The Y position of the anchor. + * @param {Number} id The id of the anchor. + */ + function addAnchor(group, x, y, id) { + // anchor shape + var anchor = new Konva.Circle({ + x: x, + y: y, + stroke: '#999', + fill: 'rgba(100,100,100,0.7', + strokeWidth: app.getStyle().getScaledStrokeWidth() / app.getScale(), + radius: app.getStyle().scale(6) / app.getScale(), + name: 'anchor', + id: id, + dragOnTop: false, + draggable: true, + visible: false + }); + // set anchor on + setAnchorOn( anchor ); + // add the anchor to the group + group.add(anchor); + } + + /** + * Get a simple clone of the input anchor. + * @param {Object} anchor The anchor to clone. + */ + function getClone( anchor ) { + // create closure to properties + var parent = anchor.getParent(); + var id = anchor.id(); + var x = anchor.x(); + var y = anchor.y(); + // create clone object + var clone = {}; + clone.getParent = function () { + return parent; + }; + clone.id = function () { + return id; + }; + clone.x = function () { + return x; + }; + clone.y = function () { + return y; + }; + return clone; + } + + /** + * Set the anchor on listeners. + * @param {Object} anchor The anchor to set on. + */ + function setAnchorOn( anchor ) { + var startAnchor = null; + + // command name based on shape type + var shapeDisplayName = dwv.tool.GetShapeDisplayName(shape); + + // drag start listener + anchor.on('dragstart.edit', function (evt) { + startAnchor = getClone(this); + // prevent bubbling upwards + evt.cancelBubble = true; + }); + // drag move listener + anchor.on('dragmove.edit', function (evt) { + if ( updateFunction ) { + updateFunction(this, image); + } + if ( this.getLayer() ) { + this.getLayer().draw(); + } + else { + console.warn("No layer to draw the anchor!"); + } + // prevent bubbling upwards + evt.cancelBubble = true; + }); + // drag end listener + anchor.on('dragend.edit', function (evt) { + var endAnchor = getClone(this); + // store the change command + var chgcmd = new dwv.tool.ChangeGroupCommand( + shapeDisplayName, updateFunction, startAnchor, endAnchor, this.getLayer(), image); + chgcmd.onExecute = drawEventCallback; + chgcmd.onUndo = drawEventCallback; + chgcmd.execute(); + app.addToUndoStack(chgcmd); + // reset start anchor + startAnchor = endAnchor; + // prevent bubbling upwards + evt.cancelBubble = true; + }); + // mouse down listener + anchor.on('mousedown touchstart', function () { + this.moveToTop(); + }); + // mouse over styling + anchor.on('mouseover.edit', function () { + // style is handled by the group + this.stroke('#ddd'); + if ( this.getLayer() ) { + this.getLayer().draw(); + } + else { + console.warn("No layer to draw the anchor!"); + } + }); + // mouse out styling + anchor.on('mouseout.edit', function () { + // style is handled by the group + this.stroke('#999'); + if ( this.getLayer() ) { + this.getLayer().draw(); + } + else { + console.warn("No layer to draw the anchor!"); + } + }); + } + + /** + * Set the anchor off listeners. + * @param {Object} anchor The anchor to set off. + */ + function setAnchorOff( anchor ) { + anchor.off('dragstart.edit'); + anchor.off('dragmove.edit'); + anchor.off('dragend.edit'); + anchor.off('mousedown touchstart'); + anchor.off('mouseover.edit'); + anchor.off('mouseout.edit'); + } +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Ellipse factory. + * @constructor + * @external Konva + */ +dwv.tool.EllipseFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 2; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 0; }; +}; + +/** + * Create an ellipse shape to be displayed. + * @param {Array} points The points from which to extract the ellipse. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.EllipseFactory.prototype.create = function (points, style, image) +{ + // calculate radius + var a = Math.abs(points[0].getX() - points[1].getX()); + var b = Math.abs(points[0].getY() - points[1].getY()); + // physical shape + var ellipse = new dwv.math.Ellipse(points[0], a, b); + // draw shape + var kshape = new Konva.Ellipse({ + x: ellipse.getCenter().getX(), + y: ellipse.getCenter().getY(), + radius: { x: ellipse.getA(), y: ellipse.getB() }, + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape" + }); + // quantification + var quant = image.quantifyEllipse( ellipse ); + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = "{surface}"; + ktext.longText = ""; + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // label + var klabel = new Konva.Label({ + x: ellipse.getCenter().getX(), + y: ellipse.getCenter().getY(), + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("ellipse-group"); + group.add(kshape); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update an ellipse shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateEllipse = function (anchor, image) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kellipse = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + // find special points + var topLeft = group.getChildren( function (node) { + return node.id() === 'topLeft'; + })[0]; + var topRight = group.getChildren( function (node) { + return node.id() === 'topRight'; + })[0]; + var bottomRight = group.getChildren( function (node) { + return node.id() === 'bottomRight'; + })[0]; + var bottomLeft = group.getChildren( function (node) { + return node.id() === 'bottomLeft'; + })[0]; + // update 'self' (undo case) and special points + switch ( anchor.id() ) { + case 'topLeft': + topLeft.x( anchor.x() ); + topLeft.y( anchor.y() ); + topRight.y( anchor.y() ); + bottomLeft.x( anchor.x() ); + break; + case 'topRight': + topRight.x( anchor.x() ); + topRight.y( anchor.y() ); + topLeft.y( anchor.y() ); + bottomRight.x( anchor.x() ); + break; + case 'bottomRight': + bottomRight.x( anchor.x() ); + bottomRight.y( anchor.y() ); + bottomLeft.y( anchor.y() ); + topRight.x( anchor.x() ); + break; + case 'bottomLeft': + bottomLeft.x( anchor.x() ); + bottomLeft.y( anchor.y() ); + bottomRight.y( anchor.y() ); + topLeft.x( anchor.x() ); + break; + default : + console.error('Unhandled anchor id: '+anchor.id()); + break; + } + // update shape + var radiusX = ( topRight.x() - topLeft.x() ) / 2; + var radiusY = ( bottomRight.y() - topRight.y() ) / 2; + var center = { 'x': topLeft.x() + radiusX, 'y': topRight.y() + radiusY }; + kellipse.position( center ); + var radiusAbs = { 'x': Math.abs(radiusX), 'y': Math.abs(radiusY) }; + if ( radiusAbs ) { + kellipse.radius( radiusAbs ); + } + // new ellipse + var ellipse = new dwv.math.Ellipse(center, radiusAbs.x, radiusAbs.y); + // update text + var quant = image.quantifyEllipse( ellipse ); + var ktext = klabel.getText(); + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var textPos = { 'x': center.x, 'y': center.y }; + klabel.position( textPos ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +/** @namespace */ +dwv.tool.filter = dwv.tool.filter || {}; + +/** + * Filter tool. + * @constructor + * @param {Array} filterList The list of filter objects. + * @param {Object} app The associated app. + */ +dwv.tool.Filter = function ( filterList, app ) +{ + /** + * Filter GUI. + * @type Object + */ + var gui = null; + /** + * Filter list + * @type Object + */ + this.filterList = filterList; + /** + * Selected filter. + * @type Object + */ + this.selectedFilter = 0; + /** + * Default filter name. + * @type String + */ + this.defaultFilterName = 0; + /** + * Display Flag. + * @type Boolean + */ + this.displayed = false; + /** + * Listener handler. + * @type Object + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Setup the filter GUI. Called at app startup. + */ + this.setup = function () + { + if ( Object.keys(this.filterList).length !== 0 ) { + gui = new dwv.gui.Filter(app); + gui.setup(this.filterList); + for( var key in this.filterList ){ + this.filterList[key].setup(); + this.filterList[key].addEventListener("filter-run", fireEvent); + this.filterList[key].addEventListener("filter-undo", fireEvent); + } + } + }; + + /** + * Display the tool. + * @param {Boolean} bool Flag to enable or not. + */ + this.display = function (bool) + { + if ( gui ) { + gui.display(bool); + } + this.displayed = bool; + // display the selected filter + this.selectedFilter.display(bool); + }; + + /** + * Initialise the filter. Called once the image is loaded. + */ + this.init = function () + { + // set the default to the first in the list + for( var key in this.filterList ){ + this.defaultFilterName = key; + break; + } + this.setSelectedFilter(this.defaultFilterName); + // init all filters + for( key in this.filterList ) { + this.filterList[key].init(); + } + // init html + if ( gui ) { + gui.initialise(); + } + return true; + }; + + /** + * Handle keydown event. + * @param {Object} event The keydown event. + */ + this.keydown = function (event) + { + app.onKeydown(event); + }; + + /** + * Add an event listener to this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type, + * will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** + * Remove an event listener from this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + /** + * Fire an event: call all associated listeners with the input event object. + * @param {Object} event The event to fire. + * @private + */ + function fireEvent (event) { + listenerHandler.fireEvent(event); + } + +}; // class dwv.tool.Filter + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.Filter.prototype.getHelp = function () +{ + return { + "title": dwv.i18n("tool.Filter.name"), + "brief": dwv.i18n("tool.Filter.brief") + }; +}; + +/** + * Get the selected filter. + * @return {Object} The selected filter. + */ +dwv.tool.Filter.prototype.getSelectedFilter = function () +{ + return this.selectedFilter; +}; + +/** + * Set the selected filter. + * @return {String} The name of the filter to select. + */ +dwv.tool.Filter.prototype.setSelectedFilter = function (name) +{ + // check if we have it + if ( !this.hasFilter(name) ) + { + throw new Error("Unknown filter: '" + name + "'"); + } + // hide last selected + if ( this.displayed ) + { + this.selectedFilter.display(false); + } + // enable new one + this.selectedFilter = this.filterList[name]; + // display the selected filter + if ( this.displayed ) + { + this.selectedFilter.display(true); + } +}; + +/** + * Get the list of filters. + * @return {Array} The list of filter objects. + */ +dwv.tool.Filter.prototype.getFilterList = function () +{ + return this.filterList; +}; + +/** + * Check if a filter is in the filter list. + * @param {String} name The name to check. + * @return {String} The filter list element for the given name. + */ +dwv.tool.Filter.prototype.hasFilter = function (name) +{ + return this.filterList[name]; +}; + +/** + * Threshold filter tool. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.filter.Threshold = function ( app ) +{ + /** + * Associated filter. + * @type Object + */ + var filter = new dwv.image.filter.Threshold(); + /** + * Filter GUI. + * @type Object + */ + var gui = new dwv.gui.Threshold(app); + /** + * Flag to know wether to reset the image or not. + * @type Boolean + */ + var resetImage = true; + /** + * Listener handler. + * @type Object + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Setup the filter GUI. Called at app startup. + */ + this.setup = function () + { + gui.setup(); + }; + + /** + * Display the filter. + * @param {Boolean} bool Flag to display or not. + */ + this.display = function (bool) + { + gui.display(bool); + // reset the image when the tool is displayed + if ( bool ) { + resetImage = true; + } + }; + + /** + * Initialise the filter. Called once the image is loaded. + */ + this.init = function () + { + gui.initialise(); + }; + + /** + * Run the filter. + * @param {Mixed} args The filter arguments. + */ + this.run = function (args) + { + filter.setMin(args.min); + filter.setMax(args.max); + // reset the image if asked + if ( resetImage ) { + filter.setOriginalImage(app.getImage()); + resetImage = false; + } + var command = new dwv.tool.RunFilterCommand(filter, app); + command.onExecute = fireEvent; + command.onUndo = fireEvent; + command.execute(); + // save command in undo stack + app.addToUndoStack(command); + }; + + /** + * Add an event listener to this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type, + * will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** + * Remove an event listener from this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + /** + * Fire an event: call all associated listeners with the input event object. + * @param {Object} event The event to fire. + * @private + */ + function fireEvent (event) { + listenerHandler.fireEvent(event); + } + +}; // class dwv.tool.filter.Threshold + + +/** + * Sharpen filter tool. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.filter.Sharpen = function ( app ) +{ + /** + * Filter GUI. + * @type Object + */ + var gui = new dwv.gui.Sharpen(app); + /** + * Listener handler. + * @type Object + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Setup the filter GUI. Called at app startup. + */ + this.setup = function () + { + gui.setup(); + }; + + /** + * Display the filter. + * @param {Boolean} bool Flag to enable or not. + */ + this.display = function (bool) + { + gui.display(bool); + }; + + /** + * Initialise the filter. Called once the image is loaded. + */ + this.init = function () + { + // nothing to do... + }; + + /** + * Run the filter. + * @param {Mixed} args The filter arguments. + */ + this.run = function (/*args*/) + { + var filter = new dwv.image.filter.Sharpen(); + filter.setOriginalImage(app.getImage()); + var command = new dwv.tool.RunFilterCommand(filter, app); + command.onExecute = fireEvent; + command.onUndo = fireEvent; + command.execute(); + // save command in undo stack + app.addToUndoStack(command); + }; + + /** + * Add an event listener to this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type, + * will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** + * Remove an event listener from this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + /** + * Fire an event: call all associated listeners with the input event object. + * @param {Object} event The event to fire. + * @private + */ + function fireEvent (event) { + listenerHandler.fireEvent(event); + } + +}; // dwv.tool.filter.Sharpen + +/** + * Sobel filter tool. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.filter.Sobel = function ( app ) +{ + /** + * Filter GUI. + * @type Object + */ + var gui = new dwv.gui.Sobel(app); + /** + * Listener handler. + * @type Object + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Setup the filter GUI. Called at app startup. + */ + this.setup = function () + { + gui.setup(); + }; + + /** + * Enable the filter. + * @param {Boolean} bool Flag to enable or not. + */ + this.display = function (bool) + { + gui.display(bool); + }; + + /** + * Initialise the filter. Called once the image is loaded. + */ + this.init = function () + { + // nothing to do... + }; + + /** + * Run the filter. + * @param {Mixed} args The filter arguments. + */ + dwv.tool.filter.Sobel.prototype.run = function (/*args*/) + { + var filter = new dwv.image.filter.Sobel(); + filter.setOriginalImage(app.getImage()); + var command = new dwv.tool.RunFilterCommand(filter, app); + command.onExecute = fireEvent; + command.onUndo = fireEvent; + command.execute(); + // save command in undo stack + app.addToUndoStack(command); + }; + + /** + * Add an event listener to this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type, + * will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** + * Remove an event listener from this class. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + /** + * Fire an event: call all associated listeners with the input event object. + * @param {Object} event The event to fire. + * @private + */ + function fireEvent (event) { + listenerHandler.fireEvent(event); + } + +}; // class dwv.tool.filter.Sobel + +/** + * Run filter command. + * @constructor + * @param {Object} filter The filter to run. + * @param {Object} app The associated application. + */ +dwv.tool.RunFilterCommand = function (filter, app) { + + /** + * Get the command name. + * @return {String} The command name. + */ + this.getName = function () { return "Filter-" + filter.getName(); }; + + /** + * Execute the command. + */ + this.execute = function () + { + // run filter and set app image + app.setImage(filter.update()); + // update display + app.render(); + // callback + this.onExecute({'type': 'filter-run', 'id': this.getName()}); + }; + + /** + * Undo the command. + */ + this.undo = function () { + // reset the image + app.setImage(filter.getOriginalImage()); + // update display + app.render(); + // callback + this.onUndo({'type': 'filter-undo', 'id': this.getName()}); + }; + +}; // RunFilterCommand class + +/** + * Handle an execute event. + * @param {Object} event The execute event with type and id. + */ +dwv.tool.RunFilterCommand.prototype.onExecute = function (/*event*/) +{ + // default does nothing. +}; +/** + * Handle an undo event. + * @param {Object} event The undo event with type and id. + */ +dwv.tool.RunFilterCommand.prototype.onUndo = function (/*event*/) +{ + // default does nothing. +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var MagicWand = MagicWand || {}; + +/** + * Floodfill painting tool. + * @constructor + * @param {Object} app The associated application. + * @external MagicWand + * @see {@link https://github.com/Tamersoul/magic-wand-js} + */ +dwv.tool.Floodfill = function(app) +{ + /** + * Original variables from external library. Used as in the lib example. + * @private + * @type Number + */ + var blurRadius = 5; + /** + * Original variables from external library. Used as in the lib example. + * @private + * @type Number + */ + var simplifyTolerant = 0; + /** + * Original variables from external library. Used as in the lib example. + * @private + * @type Number + */ + var simplifyCount = 2000; + /** + * Canvas info + * @private + * @type Object + */ + var imageInfo = null; + /** + * Object created by MagicWand lib containing border points + * @private + * @type Object + */ + var mask = null; + /** + * threshold default tolerance of the tool border + * @private + * @type Number + */ + var initialthreshold = 10; + /** + * threshold tolerance of the tool border + * @private + * @type Number + */ + var currentthreshold = null; + /** + * Closure to self: to be used by event handlers. + * @private + * @type WindowLevel + */ + var self = this; + /** + * Interaction start flag. + * @type Boolean + */ + this.started = false; + /** + * Livewire GUI. + * @type Object + */ + var gui = null; + /** + * Draw command. + * @private + * @type Object + */ + var command = null; + /** + * Current shape group. + * @private + * @type Object + */ + var shapeGroup = null; + /** + * Coordinates of the fist mousedown event. + * @private + * @type Object + */ + var initialpoint; + /** + * Floodfill border. + * @private + * @type Object + */ + var border = null; + /** + * List of parent points. + * @private + * @type Array + */ + var parentPoints = []; + /** + * Assistant variable to paint border on all slices. + * @private + * @type Boolean + */ + var extender = false; + /** + * Timeout for painting on mousemove. + * @private + */ + var painterTimeout; + /** + * Drawing style. + * @type Style + */ + this.style = new dwv.html.Style(); + + /** + * Event listeners. + * @private + */ + var listeners = []; + + /** + * Set extend option for painting border on all slices. + * @param {Boolean} The option to set + */ + this.setExtend = function(Bool){ + extender = Bool; + }; + + /** + * Get extend option for painting border on all slices. + * @return {Boolean} The actual value of of the variable to use Floodfill on museup. + */ + this.getExtend = function(){ + return extender; + }; + + /** + * Get (x, y) coordinates referenced to the canvas + * @param {Object} event The original event. + */ + var getCoord = function(event){ + return { x: event._x, y: event._y }; + }; + + /** + * Calculate border. + * @private + * @param {Object} Start point. + * @param {Number} Threshold tolerance. + */ + var calcBorder = function(points, threshold, simple){ + + parentPoints = []; + var image = { + data: imageInfo.data, + width: imageInfo.width, + height: imageInfo.height, + bytes: 4 + }; + + // var p = new dwv.math.FastPoint2D(points.x, points.y); + mask = MagicWand.floodFill(image, points.x, points.y, threshold); + mask = MagicWand.gaussBlurOnlyBorder(mask, blurRadius); + + var cs = MagicWand.traceContours(mask); + cs = MagicWand.simplifyContours(cs, simplifyTolerant, simplifyCount); + + if(cs.length > 0 && cs[0].points[0].x){ + if(simple){ + return cs[0].points; + } + for(var j=0, icsl=cs[0].points.length; jjl ; j--){ + if(!paintBorder(initialpoint, threshold)){ + break; + } + app.getViewController().decrementSliceNb(); + } + app.getViewController().setCurrentPosition(pos); + }; + + /** + * Modify tolerance threshold and redraw ROI. + * @param {Number} New threshold. + */ + this.modifyThreshold = function(modifyThreshold, shape){ + + if(!shape && shapeGroup){ + shape = shapeGroup.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + } + else{ + throw 'No shape found'; + } + + clearTimeout(painterTimeout); + painterTimeout = setTimeout( function () { + border = calcBorder(initialpoint, modifyThreshold, true); + if(!border){ + return false; + } + var arr = []; + for( var i = 0, bl = border.length; i < bl ; ++i ) + { + arr.push( border[i].x ); + arr.push( border[i].y ); + } + shape.setPoints(arr); + var shapeLayer = shape.getLayer(); + shapeLayer.draw(); + self.onThresholdChange(modifyThreshold); + }, 100); + }; + + /** + * Event fired when threshold change + * @param {Number} Current threshold + */ + this.onThresholdChange = function(/*value*/){ + // Defaults do nothing + }; + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + imageInfo = app.getImageData(); + if (!imageInfo){ return console.error('No image found');} + + self.started = true; + initialpoint = getCoord(event); + paintBorder(initialpoint, initialthreshold); + self.onThresholdChange(initialthreshold); + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + if (!self.started) + { + return; + } + var movedpoint = getCoord(event); + currentthreshold = Math.round(Math.sqrt( Math.pow((initialpoint.x-movedpoint.x), 2) + Math.pow((initialpoint.y-movedpoint.y), 2) )/2); + currentthreshold = currentthreshold < initialthreshold ? initialthreshold : currentthreshold - initialthreshold; + self.modifyThreshold(currentthreshold); + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function(/*event*/){ + self.started = false; + if(extender){ + self.extend(); + } + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(/*event*/){ + self.mouseup(/*event*/); + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + // treat as mouse down + self.mousedown(event); + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + // treat as mouse move + self.mousemove(event); + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(/*event*/){ + // treat as mouse up + self.mouseup(/*event*/); + }; + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + app.onKeydown(event); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.ColourTool(app, "ff"); + gui.setup(); + }; + + /** + * Enable the tool. + * @param {Boolean} bool The flag to enable or not. + */ + this.display = function(bool){ + if ( gui ) { + gui.display(bool); + } + // TODO why twice? + this.init(); + }; + + /** + * Initialise the tool. + */ + this.init = function() + { + if ( gui ) { + // init with the app window scale + this.style.setScale(app.getWindowScale()); + // set the default to the first in the list + this.setLineColour(this.style.getLineColour()); + // init html + gui.initialise(); + } + + return true; + }; + + /** + * Add an event listener on the app. + * @param {Object} listener The method associated with the provided event type. + */ + this.addEventListener = function (listener) + { + listeners.push(listener); + }; + + /** + * Remove an event listener from the app. + * @param {Object} listener The method associated with the provided event type. + */ + this.removeEventListener = function (listener) + { + for ( var i = 0; i < listeners.length; ++i ) + { + if ( listeners[i] === listener ) { + listeners.splice(i,1); + } + } + }; + + // Private Methods ----------------------------------------------------------- + + /** + * Fire an event: call all associated listeners. + * @param {Object} event The event to fire. + */ + function fireEvent (event) + { + for ( var i=0; i < listeners.length; ++i ) + { + listeners[i](event); + } + } + +}; // Floodfill class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.Floodfill.prototype.getHelp = function() +{ + return { + 'title': dwv.i18n("tool.Floodfill.name"), + 'brief': dwv.i18n("tool.Floodfill.brief"), + "mouse": { + "click": dwv.i18n("tool.Floodfill.click") + }, + "touch": { + "tap": dwv.i18n("tool.Floodfill.tap") + } + }; +}; + +/** + * Set the line colour of the drawing. + * @param {String} colour The colour to set. + */ +dwv.tool.Floodfill.prototype.setLineColour = function(colour) +{ + // set style var + this.style.setLineColour(colour); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * FreeHand factory. + * @constructor + * @external Konva + */ +dwv.tool.FreeHandFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 1000; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 25; }; +}; + +/** + * Create a roi shape to be displayed. + * @param {Array} points The points from which to extract the line. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.FreeHandFactory.prototype.create = function (points, style /*, image*/) +{ + // points stored the Konvajs way + var arr = []; + for( var i = 0; i < points.length; ++i ) + { + arr.push( points[i].getX() ); + arr.push( points[i].getY() ); + } + // draw shape + var kshape = new Konva.Line({ + points: arr, + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape", + tension: 0.5 + }); + + // text + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = ""; + ktext.longText = ""; + ktext.quant = null; + ktext.setText(ktext.textExpr); + + // label + var klabel = new Konva.Label({ + x: points[0].getX(), + y: points[0].getY() + 10, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("freeHand-group"); + group.add(kshape); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update a FreeHand shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateFreeHand = function (anchor /*, image*/) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kline = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + + // update self + var point = group.getChildren( function (node) { + return node.id() === anchor.id(); + })[0]; + point.x( anchor.x() ); + point.y( anchor.y() ); + // update the roi point and compensate for possible drag + // (the anchor id is the index of the point in the list) + var points = kline.points(); + points[anchor.id()] = anchor.x() - kline.x(); + points[anchor.id()+1] = anchor.y() - kline.y(); + kline.points( points ); + + // update text + var ktext = klabel.getText(); + ktext.quant = null; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var textPos = { 'x': points[0] + kline.x(), 'y': points[1] + kline.y() + 10 }; + klabel.position( textPos ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * Livewire painting tool. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.Livewire = function(app) +{ + /** + * Closure to self: to be used by event handlers. + * @private + * @type WindowLevel + */ + var self = this; + /** + * Livewire GUI. + * @type Object + */ + var gui = null; + /** + * Interaction start flag. + * @type Boolean + */ + this.started = false; + + /** + * Draw command. + * @private + * @type Object + */ + var command = null; + /** + * Current shape group. + * @private + * @type Object + */ + var shapeGroup = null; + /** + * Drawing style. + * @type Style + */ + this.style = new dwv.html.Style(); + // init with the app window scale + this.style.setScale(app.getWindowScale()); + + /** + * Path storage. Paths are stored in reverse order. + * @private + * @type Path + */ + var path = new dwv.math.Path(); + /** + * Current path storage. Paths are stored in reverse order. + * @private + * @type Path + */ + var currentPath = new dwv.math.Path(); + /** + * List of parent points. + * @private + * @type Array + */ + var parentPoints = []; + /** + * Tolerance. + * @private + * @type Number + */ + var tolerance = 5; + + /** + * Event listeners. + * @private + */ + var listeners = []; + + /** + * Clear the parent points list. + * @private + */ + function clearParentPoints() { + var nrows = app.getImage().getGeometry().getSize().getNumberOfRows(); + for( var i = 0; i < nrows; ++i ) { + parentPoints[i] = []; + } + } + + /** + * Clear the stored paths. + * @private + */ + function clearPaths() { + path = new dwv.math.Path(); + currentPath = new dwv.math.Path(); + } + + /** + * Scissor representation. + * @private + * @type Scissors + */ + var scissors = new dwv.math.Scissors(); + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + // first time + if( !self.started ) { + self.started = true; + self.x0 = event._x; + self.y0 = event._y; + // clear vars + clearPaths(); + clearParentPoints(); + // do the training from the first point + var p = new dwv.math.FastPoint2D(event._x, event._y); + scissors.doTraining(p); + // add the initial point to the path + var p0 = new dwv.math.Point2D(event._x, event._y); + path.addPoint(p0); + path.addControlPoint(p0); + } + else { + // final point: at 'tolerance' of the initial point + if( (Math.abs(event._x - self.x0) < tolerance) && (Math.abs(event._y - self.y0) < tolerance) ) { + // draw + self.mousemove(event); + // listen + command.onExecute = fireEvent; + command.onUndo = fireEvent; + // debug + console.log("Done."); + // save command in undo stack + app.addToUndoStack(command); + // set flag + self.started = false; + } + // anchor point + else { + path = currentPath; + clearParentPoints(); + var pn = new dwv.math.FastPoint2D(event._x, event._y); + scissors.doTraining(pn); + path.addControlPoint(currentPath.getPoint(0)); + } + } + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + if (!self.started) + { + return; + } + // set the point to find the path to + var p = new dwv.math.FastPoint2D(event._x, event._y); + scissors.setPoint(p); + // do the work + var results = 0; + var stop = false; + while( !parentPoints[p.y][p.x] && !stop) + { + console.log("Getting ready..."); + results = scissors.doWork(); + + if( results.length === 0 ) { + stop = true; + } + else { + // fill parents + for( var i = 0; i < results.length-1; i+=2 ) { + var _p = results[i]; + var _q = results[i+1]; + parentPoints[_p.y][_p.x] = _q; + } + } + } + console.log("Ready!"); + + // get the path + currentPath = new dwv.math.Path(); + stop = false; + while (p && !stop) { + currentPath.addPoint(new dwv.math.Point2D(p.x, p.y)); + if(!parentPoints[p.y]) { + stop = true; + } + else { + if(!parentPoints[p.y][p.x]) { + stop = true; + } + else { + p = parentPoints[p.y][p.x]; + } + } + } + currentPath.appenPath(path); + + // remove previous draw + if ( shapeGroup ) { + shapeGroup.destroy(); + } + // create shape + var factory = new dwv.tool.RoiFactory(); + shapeGroup = factory.create(currentPath.pointArray, self.style); + shapeGroup.id( dwv.math.guid() ); + + // get the position group + var posGroup = app.getDrawController().getCurrentPosGroup(); + // add shape group to position group + posGroup.add(shapeGroup); + + // draw shape command + command = new dwv.tool.DrawGroupCommand(shapeGroup, "livewire", + app.getDrawController().getDrawLayer()); + // draw + command.execute(); + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function(/*event*/){ + // nothing to do + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(event){ + // treat as mouse up + self.mouseup(event); + }; + + /** + * Handle double click event. + * @param {Object} event The double click event. + */ + this.dblclick = function(/*event*/){ + console.log("dblclick"); + // save command in undo stack + app.addToUndoStack(command); + // set flag + self.started = false; + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + // treat as mouse down + self.mousedown(event); + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + // treat as mouse move + self.mousemove(event); + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(event){ + // treat as mouse up + self.mouseup(event); + }; + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + app.onKeydown(event); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.ColourTool(app, "lw"); + gui.setup(); + }; + + /** + * Enable the tool. + * @param {Boolean} bool The flag to enable or not. + */ + this.display = function(bool){ + if ( gui ) { + gui.display(bool); + } + // start scissors if displayed + if (bool) { + //scissors = new dwv.math.Scissors(); + var size = app.getImage().getGeometry().getSize(); + scissors.setDimensions( + size.getNumberOfColumns(), + size.getNumberOfRows() ); + scissors.setData(app.getImageData().data); + } + }; + + /** + * Initialise the tool. + */ + this.init = function() + { + if ( gui ) { + // init with the app window scale + this.style.setScale(app.getWindowScale()); + // set the default to the first in the list + this.setLineColour(this.style.getLineColour()); + // init html + gui.initialise(); + } + + return true; + }; + + /** + * Add an event listener on the app. + * @param {Object} listener The method associated with the provided event type. + */ + this.addEventListener = function (listener) + { + listeners.push(listener); + }; + + /** + * Remove an event listener from the app. + * @param {Object} listener The method associated with the provided event type. + */ + this.removeEventListener = function (listener) + { + for ( var i = 0; i < listeners.length; ++i ) + { + if ( listeners[i] === listener ) { + listeners.splice(i,1); + } + } + }; + + // Private Methods ----------------------------------------------------------- + + /** + * Fire an event: call all associated listeners. + * @param {Object} event The event to fire. + */ + function fireEvent (event) + { + for ( var i=0; i < listeners.length; ++i ) + { + listeners[i](event); + } + } + +}; // Livewire class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.Livewire.prototype.getHelp = function() +{ + return { + "title": dwv.i18n("tool.Livewire.name"), + "brief": dwv.i18n("tool.Livewire.brief") + }; +}; + +/** + * Set the line colour of the drawing. + * @param {String} colour The colour to set. + */ +dwv.tool.Livewire.prototype.setLineColour = function(colour) +{ + // set style var + this.style.setLineColour(colour); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Protractor factory. + * @constructor + * @external Konva + */ +dwv.tool.ProtractorFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 3; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 500; }; +}; + +/** + * Create a protractor shape to be displayed. + * @param {Array} points The points from which to extract the protractor. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.ProtractorFactory.prototype.create = function (points, style/*, image*/) +{ + // physical shape + var line0 = new dwv.math.Line(points[0], points[1]); + // points stored the Konvajs way + var pointsArray = []; + for( var i = 0; i < points.length; ++i ) + { + pointsArray.push( points[i].getX() ); + pointsArray.push( points[i].getY() ); + } + // draw shape + var kshape = new Konva.Line({ + points: pointsArray, + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape" + }); + var group = new Konva.Group(); + group.name("protractor-group"); + group.add(kshape); + group.visible(true); // dont inherit + // text and decoration + if ( points.length === 3 ) { + var line1 = new dwv.math.Line(points[1], points[2]); + // larger hitfunc + kshape.hitFunc( function (context) { + context.beginPath(); + context.moveTo( points[0].getX(), points[0].getY() ); + context.lineTo( points[1].getX(), points[1].getY() ); + context.lineTo( points[2].getX(), points[2].getY() ); + context.closePath(); + context.fillStrokeShape(this); + }); + // quantification + var angle = dwv.math.getAngle(line0, line1); + var inclination = line0.getInclination(); + if ( angle > 180 ) { + angle = 360 - angle; + inclination += angle; + } + + // quantification + var quant = { "angle": { "value": angle, "unit": dwv.i18n("unit.degree")} }; + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = "{angle}"; + ktext.longText = ""; + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + + // label + var midX = ( line0.getMidpoint().getX() + line1.getMidpoint().getX() ) / 2; + var midY = ( line0.getMidpoint().getY() + line1.getMidpoint().getY() ) / 2; + var klabel = new Konva.Label({ + x: midX, + y: midY - 15, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // arc + var radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100; + var karc = new Konva.Arc({ + innerRadius: radius, + outerRadius: radius, + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + angle: angle, + rotation: -inclination, + x: points[1].getX(), + y: points[1].getY(), + name: "shape-arc" + }); + // add to group + group.add(klabel); + group.add(karc); + } + // return group + return group; +}; + +/** + * Update a protractor shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateProtractor = function (anchor/*, image*/) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kline = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + // associated arc + var karc = group.getChildren( function (node) { + return node.name() === 'shape-arc'; + })[0]; + // find special points + var begin = group.getChildren( function (node) { + return node.id() === 'begin'; + })[0]; + var mid = group.getChildren( function (node) { + return node.id() === 'mid'; + })[0]; + var end = group.getChildren( function (node) { + return node.id() === 'end'; + })[0]; + // update special points + switch ( anchor.id() ) { + case 'begin': + begin.x( anchor.x() ); + begin.y( anchor.y() ); + break; + case 'mid': + mid.x( anchor.x() ); + mid.y( anchor.y() ); + break; + case 'end': + end.x( anchor.x() ); + end.y( anchor.y() ); + break; + } + // update shape and compensate for possible drag + // note: shape.position() and shape.size() won't work... + var bx = begin.x() - kline.x(); + var by = begin.y() - kline.y(); + var mx = mid.x() - kline.x(); + var my = mid.y() - kline.y(); + var ex = end.x() - kline.x(); + var ey = end.y() - kline.y(); + kline.points( [bx,by,mx,my,ex,ey] ); + // larger hitfunc + kline.hitFunc( function (context) { + context.beginPath(); + context.moveTo( bx, by ); + context.lineTo( mx, my ); + context.lineTo( ex, ey ); + context.closePath(); + context.fillStrokeShape(this); + }); + // update text + var p2d0 = new dwv.math.Point2D(begin.x(), begin.y()); + var p2d1 = new dwv.math.Point2D(mid.x(), mid.y()); + var p2d2 = new dwv.math.Point2D(end.x(), end.y()); + var line0 = new dwv.math.Line(p2d0, p2d1); + var line1 = new dwv.math.Line(p2d1, p2d2); + var angle = dwv.math.getAngle(line0, line1); + var inclination = line0.getInclination(); + if ( angle > 180 ) { + angle = 360 - angle; + inclination += angle; + } + + // update text + var quant = { "angle": { "value": angle, "unit": dwv.i18n("unit.degree")} }; + var ktext = klabel.getText(); + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var midX = ( line0.getMidpoint().getX() + line1.getMidpoint().getX() ) / 2; + var midY = ( line0.getMidpoint().getY() + line1.getMidpoint().getY() ) / 2; + var textPos = { 'x': midX, 'y': midY - 15 }; + klabel.position( textPos ); + + // arc + var radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100; + karc.innerRadius(radius); + karc.outerRadius(radius); + karc.angle(angle); + karc.rotation(-inclination); + var arcPos = { 'x': mid.x(), 'y': mid.y() }; + karc.position(arcPos); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Rectangle factory. + * @constructor + * @external Konva + */ +dwv.tool.RectangleFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 2; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 0; }; +}; + +/** + * Create a rectangle shape to be displayed. + * @param {Array} points The points from which to extract the rectangle. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.RectangleFactory.prototype.create = function (points, style, image) +{ + // physical shape + var rectangle = new dwv.math.Rectangle(points[0], points[1]); + // draw shape + var kshape = new Konva.Rect({ + x: rectangle.getBegin().getX(), + y: rectangle.getBegin().getY(), + width: rectangle.getWidth(), + height: rectangle.getHeight(), + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape" + }); + // quantification + var quant = image.quantifyRect( rectangle ); + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = "{surface}"; + ktext.longText = ""; + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + + // label + var klabel = new Konva.Label({ + x: rectangle.getBegin().getX(), + y: rectangle.getEnd().getY() + 10, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("rectangle-group"); + group.add(kshape); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update a rectangle shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateRect = function (anchor, image) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var krect = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + // find special points + var topLeft = group.getChildren( function (node) { + return node.id() === 'topLeft'; + })[0]; + var topRight = group.getChildren( function (node) { + return node.id() === 'topRight'; + })[0]; + var bottomRight = group.getChildren( function (node) { + return node.id() === 'bottomRight'; + })[0]; + var bottomLeft = group.getChildren( function (node) { + return node.id() === 'bottomLeft'; + })[0]; + // update 'self' (undo case) and special points + switch ( anchor.id() ) { + case 'topLeft': + topLeft.x( anchor.x() ); + topLeft.y( anchor.y() ); + topRight.y( anchor.y() ); + bottomLeft.x( anchor.x() ); + break; + case 'topRight': + topRight.x( anchor.x() ); + topRight.y( anchor.y() ); + topLeft.y( anchor.y() ); + bottomRight.x( anchor.x() ); + break; + case 'bottomRight': + bottomRight.x( anchor.x() ); + bottomRight.y( anchor.y() ); + bottomLeft.y( anchor.y() ); + topRight.x( anchor.x() ); + break; + case 'bottomLeft': + bottomLeft.x( anchor.x() ); + bottomLeft.y( anchor.y() ); + bottomRight.y( anchor.y() ); + topLeft.x( anchor.x() ); + break; + default : + console.error('Unhandled anchor id: '+anchor.id()); + break; + } + // update shape + krect.position(topLeft.position()); + var width = topRight.x() - topLeft.x(); + var height = bottomLeft.y() - topLeft.y(); + if ( width && height ) { + krect.size({'width': width, 'height': height}); + } + // new rect + var p2d0 = new dwv.math.Point2D(topLeft.x(), topLeft.y()); + var p2d1 = new dwv.math.Point2D(bottomRight.x(), bottomRight.y()); + var rect = new dwv.math.Rectangle(p2d0, p2d1); + // update text + var quant = image.quantifyRect( rect ); + var ktext = klabel.getText(); + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var textPos = { 'x': rect.getBegin().getX(), 'y': rect.getEnd().getY() + 10 }; + klabel.position( textPos ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * ROI factory. + * @constructor + * @external Konva + */ +dwv.tool.RoiFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 50; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 100; }; +}; + +/** + * Create a roi shape to be displayed. + * @param {Array} points The points from which to extract the line. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.RoiFactory.prototype.create = function (points, style /*, image*/) +{ + // physical shape + var roi = new dwv.math.ROI(); + // add input points to the ROI + roi.addPoints(points); + // points stored the Konvajs way + var arr = []; + for( var i = 0; i < roi.getLength(); ++i ) + { + arr.push( roi.getPoint(i).getX() ); + arr.push( roi.getPoint(i).getY() ); + } + // draw shape + var kshape = new Konva.Line({ + points: arr, + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape", + closed: true + }); + + // text + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = ""; + ktext.longText = ""; + ktext.quant = null; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + + // label + var klabel = new Konva.Label({ + x: roi.getPoint(0).getX(), + y: roi.getPoint(0).getY() + 10, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("roi-group"); + group.add(kshape); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update a roi shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateRoi = function (anchor /*, image*/) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kroi = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + + // update self + var point = group.getChildren( function (node) { + return node.id() === anchor.id(); + })[0]; + point.x( anchor.x() ); + point.y( anchor.y() ); + // update the roi point and compensate for possible drag + // (the anchor id is the index of the point in the list) + var points = kroi.points(); + points[anchor.id()] = anchor.x() - kroi.x(); + points[anchor.id()+1] = anchor.y() - kroi.y(); + kroi.points( points ); + + // update text + var ktext = klabel.getText(); + ktext.quant = null; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var textPos = { 'x': points[0] + kroi.x(), 'y': points[1] + kroi.y() + 10 }; + klabel.position( textPos ); + +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; +// external +var Konva = Konva || {}; + +/** + * Ruler factory. + * @constructor + * @external Konva + */ +dwv.tool.RulerFactory = function () +{ + /** + * Get the number of points needed to build the shape. + * @return {Number} The number of points. + */ + this.getNPoints = function () { return 2; }; + /** + * Get the timeout between point storage. + * @return {Number} The timeout in milliseconds. + */ + this.getTimeout = function () { return 0; }; +}; + +/** + * Create a ruler shape to be displayed. + * @param {Array} points The points from which to extract the line. + * @param {Object} style The drawing style. + * @param {Object} image The associated image. + */ +dwv.tool.RulerFactory.prototype.create = function (points, style, image) +{ + // physical shape + var line = new dwv.math.Line(points[0], points[1]); + // draw shape + var kshape = new Konva.Line({ + points: [line.getBegin().getX(), line.getBegin().getY(), + line.getEnd().getX(), line.getEnd().getY() ], + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape" + }); + + // tick begin + var linePerp0 = dwv.math.getPerpendicularLine( line, points[0], 10 ); + var ktick0 = new Konva.Line({ + points: [linePerp0.getBegin().getX(), linePerp0.getBegin().getY(), + linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ], + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape-tick0" + }); + + // tick end + var linePerp1 = dwv.math.getPerpendicularLine( line, points[1], 10 ); + var ktick1 = new Konva.Line({ + points: [linePerp1.getBegin().getX(), linePerp1.getBegin().getY(), + linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ], + stroke: style.getLineColour(), + strokeWidth: style.getScaledStrokeWidth(), + name: "shape-tick1" + }); + + // larger hitfunc + kshape.hitFunc( function (context) { + context.beginPath(); + context.moveTo( linePerp0.getBegin().getX(), linePerp0.getBegin().getY() ); + context.lineTo( linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ); + context.lineTo( linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ); + context.lineTo( linePerp1.getBegin().getX(), linePerp1.getBegin().getY() ); + context.closePath(); + context.fillStrokeShape(this); + }); + + // quantification + var quant = image.quantifyLine( line ); + var ktext = new Konva.Text({ + fontSize: style.getScaledFontSize(), + fontFamily: style.getFontFamily(), + fill: style.getLineColour(), + name: "text" + }); + ktext.textExpr = "{length}"; + ktext.longText = ""; + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // label + var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1; + var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5; + var klabel = new Konva.Label({ + x: line.getEnd().getX() + dX * 25, + y: line.getEnd().getY() + dY * 15, + name: "label" + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + + // return group + var group = new Konva.Group(); + group.name("ruler-group"); + group.add(kshape); + group.add(ktick0); + group.add(ktick1); + group.add(klabel); + group.visible(true); // dont inherit + return group; +}; + +/** + * Update a ruler shape. + * @param {Object} anchor The active anchor. + * @param {Object} image The associated image. + */ +dwv.tool.UpdateRuler = function (anchor, image) +{ + // parent group + var group = anchor.getParent(); + // associated shape + var kline = group.getChildren( function (node) { + return node.name() === 'shape'; + })[0]; + // associated tick0 + var ktick0 = group.getChildren( function (node) { + return node.name() === 'shape-tick0'; + })[0]; + // associated tick1 + var ktick1 = group.getChildren( function (node) { + return node.name() === 'shape-tick1'; + })[0]; + // associated label + var klabel = group.getChildren( function (node) { + return node.name() === 'label'; + })[0]; + // find special points + var begin = group.getChildren( function (node) { + return node.id() === 'begin'; + })[0]; + var end = group.getChildren( function (node) { + return node.id() === 'end'; + })[0]; + // update special points + switch ( anchor.id() ) { + case 'begin': + begin.x( anchor.x() ); + begin.y( anchor.y() ); + break; + case 'end': + end.x( anchor.x() ); + end.y( anchor.y() ); + break; + } + // update shape and compensate for possible drag + // note: shape.position() and shape.size() won't work... + var bx = begin.x() - kline.x(); + var by = begin.y() - kline.y(); + var ex = end.x() - kline.x(); + var ey = end.y() - kline.y(); + kline.points( [bx,by,ex,ey] ); + // new line + var p2d0 = new dwv.math.Point2D(begin.x(), begin.y()); + var p2d1 = new dwv.math.Point2D(end.x(), end.y()); + var line = new dwv.math.Line(p2d0, p2d1); + // tick + var p2b = new dwv.math.Point2D(bx, by); + var p2e = new dwv.math.Point2D(ex, ey); + var linePerp0 = dwv.math.getPerpendicularLine( line, p2b, 10 ); + ktick0.points( [linePerp0.getBegin().getX(), linePerp0.getBegin().getY(), + linePerp0.getEnd().getX(), linePerp0.getEnd().getY()] ); + var linePerp1 = dwv.math.getPerpendicularLine( line, p2e, 10 ); + ktick1.points( [linePerp1.getBegin().getX(), linePerp1.getBegin().getY(), + linePerp1.getEnd().getX(), linePerp1.getEnd().getY()] ); + // larger hitfunc + kline.hitFunc( function (context) { + context.beginPath(); + context.moveTo( linePerp0.getBegin().getX(), linePerp0.getBegin().getY() ); + context.lineTo( linePerp0.getEnd().getX(), linePerp0.getEnd().getY() ); + context.lineTo( linePerp1.getEnd().getX(), linePerp1.getEnd().getY() ); + context.lineTo( linePerp1.getBegin().getX(), linePerp1.getBegin().getY() ); + context.closePath(); + context.fillStrokeShape(this); + }); + // update text + var quant = image.quantifyLine( line ); + var ktext = klabel.getText(); + ktext.quant = quant; + ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant)); + // update position + var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1; + var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5; + var textPos = { + 'x': line.getEnd().getX() + dX * 25, + 'y': line.getEnd().getY() + dY * 15 }; + klabel.position( textPos ); +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * Scroll class. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.Scroll = function(app) +{ + /** + * Closure to self: to be used by event handlers. + * @private + * @type WindowLevel + */ + var self = this; + /** + * Scroll GUI. + * @type Object + */ + var gui = null; + /** + * Interaction start flag. + * @type Boolean + */ + this.started = false; + // touch timer ID (created by setTimeout) + var touchTimerID = null; + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + // stop viewer if playing + if ( app.getViewController().isPlaying() ) { + app.getViewController().stop(); + } + // start flag + self.started = true; + // first position + self.x0 = event._x; + self.y0 = event._y; + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + if (!self.started) { + return; + } + + // difference to last Y position + var diffY = event._y - self.y0; + var yMove = (Math.abs(diffY) > 15); + // do not trigger for small moves + if( yMove ) { + // update GUI + if( diffY > 0 ) { + app.getViewController().decrementSliceNb(); + } + else { + app.getViewController().incrementSliceNb(); + } + } + + // difference to last X position + var diffX = event._x - self.x0; + var xMove = (Math.abs(diffX) > 15); + // do not trigger for small moves + if( xMove ) { + // update GUI + if( diffX > 0 ) { + app.getViewController().incrementFrameNb(); + } + else { + app.getViewController().decrementFrameNb(); + } + } + + // reset origin point + if (xMove) { + self.x0 = event._x; + } + if (yMove) { + self.y0 = event._y; + } + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function(/*event*/){ + if (self.started) + { + // stop recording + self.started = false; + } + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(event){ + self.mouseup(event); + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + // long touch triggers the dblclick + touchTimerID = setTimeout(self.dblclick, 500); + // call mouse equivalent + self.mousedown(event); + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + // abort timer if move + if (touchTimerID !== null) { + clearTimeout(touchTimerID); + touchTimerID = null; + } + // call mouse equivalent + self.mousemove(event); + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(event){ + // abort timer + if (touchTimerID !== null) { + clearTimeout(touchTimerID); + touchTimerID = null; + } + // call mouse equivalent + self.mouseup(event); + }; + + /** + * Handle mouse scroll event (fired by Firefox). + * @param {Object} event The mouse scroll event. + */ + this.DOMMouseScroll = function (event) { + // ev.detail on firefox is 3 + if ( event.detail < 0 ) { + mouseScroll(true); + } else { + mouseScroll(false); + } + }; + + /** + * Handle mouse wheel event. + * @param {Object} event The mouse wheel event. + */ + this.mousewheel = function (event) { + // ev.wheelDelta on chrome is 120 + if ( event.wheelDelta > 0 ) { + mouseScroll(true); + } else { + mouseScroll(false); + } + }; + + /** + * Mouse scroll action. + * @param {Boolean} up True to increment, false to decrement. + */ + function mouseScroll (up) { + var hasSlices = (app.getImage().getGeometry().getSize().getNumberOfSlices() !== 1); + var hasFrames = (app.getImage().getNumberOfFrames() !== 1); + if ( up ) { + if (hasSlices) { + app.getViewController().incrementSliceNb(); + } else if (hasFrames) { + app.getViewController().incrementFrameNb(); + } + } else { + if (hasSlices) { + app.getViewController().decrementSliceNb(); + } else if (hasFrames) { + app.getViewController().decrementFrameNb(); + } + } + } + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + app.onKeydown(event); + }; + /** + * Handle double click. + * @param {Object} event The key down event. + */ + this.dblclick = function (/*event*/) { + app.getViewController().play(); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.Scroll(app); + gui.setup(); + }; + + /** + * Enable the tool. + * @param {Boolean} bool The flag to enable or not. + */ + this.display = function(bool){ + if ( gui ) { + gui.display(bool); + } + }; + + /** + * Initialise the tool. + */ + this.init = function() { + if ( app.isMonoSliceData() && app.getImage().getNumberOfFrames() === 1 ) { + return false; + } + return true; + }; + +}; // Scroll class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.Scroll.prototype.getHelp = function() +{ + return { + "title": dwv.i18n("tool.Scroll.name"), + "brief": dwv.i18n("tool.Scroll.brief"), + "mouse": { + "mouse_drag": dwv.i18n("tool.Scroll.mouse_drag"), + "double_click": dwv.i18n("tool.Scroll.double_click") + }, + "touch": { + 'touch_drag': dwv.i18n("tool.Scroll.touch_drag"), + 'tap_and_hold': dwv.i18n("tool.Scroll.tap_and_hold") + } + }; +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * Tool box. + * @constructor + * @param {Array} toolList The list of tool objects. + * @param {Object} gui The associated gui. + */ +dwv.tool.Toolbox = function( toolList, app ) +{ + /** + * Toolbox GUI. + * @type Object + */ + var gui = null; + /** + * Selected tool. + * @type Object + */ + var selectedTool = null; + /** + * Default tool name. + * @type String + */ + var defaultToolName = null; + + /** + * Get the list of tools. + * @return {Array} The list of tool objects. + */ + this.getToolList = function () + { + return toolList; + }; + + /** + * Get the selected tool. + * @return {Object} The selected tool. + */ + this.getSelectedTool = function () + { + return selectedTool; + }; + + /** + * Setup the toolbox GUI. + */ + this.setup = function () + { + if ( Object.keys(toolList).length !== 0 ) { + gui = new dwv.gui.Toolbox(app); + gui.setup(toolList); + + for( var key in toolList ) { + toolList[key].setup(); + } + } + }; + + /** + * Display the toolbox. + * @param {Boolean} bool Flag to display or not. + */ + this.display = function (bool) + { + if ( Object.keys(toolList).length !== 0 && gui ) { + gui.display(bool); + } + }; + + /** + * Initialise the tool box. + */ + this.init = function () + { + var keys = Object.keys(toolList); + // check if we have tools + if ( keys.length === 0 ) { + return; + } + // init all tools + defaultToolName = ""; + var displays = []; + var display = null; + for( var key in toolList ) { + display = toolList[key].init(); + if ( display && defaultToolName === "" ) { + defaultToolName = key; + } + displays.push(display); + } + this.setSelectedTool(defaultToolName); + // init html + if ( gui ) { + gui.initialise(displays); + } + }; + + /** + * Set the selected tool. + * @return {String} The name of the tool to select. + */ + this.setSelectedTool = function (name) + { + // check if we have it + if( !this.hasTool(name) ) + { + throw new Error("Unknown tool: '" + name + "'"); + } + // hide last selected + if( selectedTool ) + { + selectedTool.display(false); + } + // enable new one + selectedTool = toolList[name]; + // display it + selectedTool.display(true); + }; + + /** + * Reset the tool box. + */ + this.reset = function () + { + // hide last selected + if ( selectedTool ) { + selectedTool.display(false); + } + selectedTool = null; + defaultToolName = null; + }; +}; + +/** + * Check if a tool is in the tool list. + * @param {String} name The name to check. + * @return {String} The tool list element for the given name. + */ +dwv.tool.Toolbox.prototype.hasTool = function (name) +{ + return this.getToolList()[name]; +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * UndoStack class. + * @constructor + */ +dwv.tool.UndoStack = function (app) +{ + /** + * Undo GUI. + * @type Object + */ + var gui = new dwv.gui.Undo(app); + /** + * Array of commands. + * @private + * @type Array + */ + var stack = []; + + /** + * Get the stack. + * @return {Array} The list of stored commands. + */ + this.getStack = function () { return stack; }; + + /** + * Current command index. + * @private + * @type Number + */ + var curCmdIndex = 0; + + /** + * Add a command to the stack. + * @param {Object} cmd The command to add. + */ + this.add = function(cmd) + { + // clear commands after current index + stack = stack.slice(0,curCmdIndex); + // store command + stack.push(cmd); + //stack[curCmdIndex] = cmd; + // increment index + ++curCmdIndex; + // add command to display history + gui.addCommandToUndoHtml(cmd.getName()); + }; + + /** + * Undo the last command. + */ + this.undo = function() + { + // a bit inefficient... + if( curCmdIndex > 0 ) + { + // decrement command index + --curCmdIndex; + // undo last command + stack[curCmdIndex].undo(); + // disable last in display history + gui.enableInUndoHtml(false); + } + }; + + /** + * Redo the last command. + */ + this.redo = function() + { + if( curCmdIndex < stack.length ) + { + // run last command + stack[curCmdIndex].execute(); + // increment command index + ++curCmdIndex; + // enable next in display history + gui.enableInUndoHtml(true); + } + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui.setup(); + }; + + /** + * Initialise the tool GUI. + */ + this.initialise = function () + { + gui.initialise(); + }; + +}; // UndoStack class + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * WindowLevel tool: handle window/level related events. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.WindowLevel = function(app) +{ + /** + * Closure to self: to be used by event handlers. + * @private + * @type WindowLevel + */ + var self = this; + /** + * WindowLevel GUI. + * @type Object + */ + var gui = null; + /** + * Interaction start flag. + * @type Boolean + */ + this.started = false; + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + // set start flag + self.started = true; + // store initial position + self.x0 = event._x; + self.y0 = event._y; + // update GUI + app.getViewController().setCurrentPosition2D(event._x, event._y); + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + // check start flag + if( !self.started ) { + return; + } + // difference to last position + var diffX = event._x - self.x0; + var diffY = self.y0 - event._y; + // calculate new window level + var windowCenter = parseInt(app.getViewController().getWindowLevel().center, 10) + diffY; + var windowWidth = parseInt(app.getViewController().getWindowLevel().width, 10) + diffX; + + // add the manual preset to the view + app.getViewController().addWindowLevelPresets( { "manual": { + "wl": new dwv.image.WindowLevel(windowCenter, windowWidth), + "name": "manual"} } ); + app.getViewController().setWindowLevelPreset("manual"); + + // update gui + if ( gui ) { + // initialise to add the manual preset + gui.initialise(); + // set selected preset + dwv.gui.setSelected(app.getElement("presetSelect"), "manual"); + } + + // store position + self.x0 = event._x; + self.y0 = event._y; + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function(/*event*/){ + // set start flag + if( self.started ) { + self.started = false; + } + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(event){ + // treat as mouse up + self.mouseup(event); + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + self.mousedown(event); + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + self.mousemove(event); + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(event){ + self.mouseup(event); + }; + + /** + * Handle double click event. + * @param {Object} event The double click event. + */ + this.dblclick = function(event){ + // update GUI + app.getViewController().setWindowLevel( + parseInt(app.getImage().getRescaledValue( + event._x, event._y, app.getViewController().getCurrentPosition().k), 10), + parseInt(app.getViewController().getWindowLevel().width, 10) ); + }; + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + // let the app handle it + app.onKeydown(event); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.WindowLevel(app); + gui.setup(); + }; + + /** + * Display the tool. + * @param {Boolean} bool The flag to display or not. + */ + this.display = function (bool) + { + if ( gui ) + { + if( app.getImage().getPhotometricInterpretation().match(/MONOCHROME/) !== null ) { + gui.display(bool); + } + else { + gui.display(false); + } + } + }; + + /** + * Initialise the tool. + */ + this.init = function() { + if ( gui ) { + gui.initialise(); + } + return true; + }; +}; // WindowLevel class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.WindowLevel.prototype.getHelp = function() +{ + return { + "title": dwv.i18n("tool.WindowLevel.name"), + "brief": dwv.i18n("tool.WindowLevel.brief"), + "mouse": { + "mouse_drag": dwv.i18n("tool.WindowLevel.mouse_drag"), + "double_click": dwv.i18n("tool.WindowLevel.double_click") + }, + "touch": { + "touch_drag": dwv.i18n("tool.WindowLevel.touch_drag") + } + }; +}; + +// namespaces +var dwv = dwv || {}; +dwv.tool = dwv.tool || {}; + +/** + * ZoomAndPan class. + * @constructor + * @param {Object} app The associated application. + */ +dwv.tool.ZoomAndPan = function(app) +{ + /** + * Closure to self: to be used by event handlers. + * @private + * @type Object + */ + var self = this; + /** + * ZoomAndPan GUI. + * @type Object + */ + var gui = null; + /** + * Interaction start flag. + * @type Boolean + */ + this.started = false; + + /** + * Handle mouse down event. + * @param {Object} event The mouse down event. + */ + this.mousedown = function(event){ + self.started = true; + // first position + self.x0 = event._xs; + self.y0 = event._ys; + }; + + /** + * Handle two touch down event. + * @param {Object} event The touch down event. + */ + this.twotouchdown = function(event){ + self.started = true; + // store first point + self.x0 = event._x; + self.y0 = event._y; + // first line + var point0 = new dwv.math.Point2D(event._x, event._y); + var point1 = new dwv.math.Point2D(event._x1, event._y1); + self.line0 = new dwv.math.Line(point0, point1); + self.midPoint = self.line0.getMidpoint(); + }; + + /** + * Handle mouse move event. + * @param {Object} event The mouse move event. + */ + this.mousemove = function(event){ + if (!self.started) + { + return; + } + // calculate translation + var tx = event._xs - self.x0; + var ty = event._ys - self.y0; + // apply translation + //app.translate(tx, ty); + app.stepTranslate(tx, ty); + // reset origin point + self.x0 = event._xs; + self.y0 = event._ys; + }; + + /** + * Handle two touch move event. + * @param {Object} event The touch move event. + */ + this.twotouchmove = function(event){ + if (!self.started) + { + return; + } + var point0 = new dwv.math.Point2D(event._x, event._y); + var point1 = new dwv.math.Point2D(event._x1, event._y1); + var newLine = new dwv.math.Line(point0, point1); + var lineRatio = newLine.getLength() / self.line0.getLength(); + + if( lineRatio === 1 ) + { + // scroll mode + // difference to last position + var diffY = event._y - self.y0; + // do not trigger for small moves + if( Math.abs(diffY) < 15 ) { + return; + } + // update GUI + if( diffY > 0 ) { + app.getViewController().incrementSliceNb(); + } + else { + app.getViewController().decrementSliceNb(); + } + } + else + { + // zoom mode + var zoom = (lineRatio - 1) / 2; + if( Math.abs(zoom) % 0.1 <= 0.05 ) { + app.stepZoom(zoom, event._xs, event._ys); + } + } + }; + + /** + * Handle mouse up event. + * @param {Object} event The mouse up event. + */ + this.mouseup = function(/*event*/){ + if (self.started) + { + // stop recording + self.started = false; + } + }; + + /** + * Handle mouse out event. + * @param {Object} event The mouse out event. + */ + this.mouseout = function(event){ + self.mouseup(event); + }; + + /** + * Handle touch start event. + * @param {Object} event The touch start event. + */ + this.touchstart = function(event){ + var touches = event.targetTouches; + if( touches.length === 1 ){ + self.mousedown(event); + } + else if( touches.length === 2 ){ + self.twotouchdown(event); + } + }; + + /** + * Handle touch move event. + * @param {Object} event The touch move event. + */ + this.touchmove = function(event){ + var touches = event.targetTouches; + if( touches.length === 1 ){ + self.mousemove(event); + } + else if( touches.length === 2 ){ + self.twotouchmove(event); + } + }; + + /** + * Handle touch end event. + * @param {Object} event The touch end event. + */ + this.touchend = function(event){ + self.mouseup(event); + }; + + /** + * Handle mouse scroll event (fired by Firefox). + * @param {Object} event The mouse scroll event. + */ + this.DOMMouseScroll = function(event){ + // ev.detail on firefox is 3 + var step = - event.detail / 30; + app.stepZoom(step, event._xs, event._ys); + }; + + /** + * Handle mouse wheel event. + * @param {Object} event The mouse wheel event. + */ + this.mousewheel = function(event){ + // ev.wheelDelta on chrome is 120 + var step = event.wheelDelta / 1200; + app.stepZoom(step, event._xs, event._ys); + }; + + /** + * Handle key down event. + * @param {Object} event The key down event. + */ + this.keydown = function(event){ + app.onKeydown(event); + }; + + /** + * Setup the tool GUI. + */ + this.setup = function () + { + gui = new dwv.gui.ZoomAndPan(app); + gui.setup(); + }; + + /** + * Enable the tool. + * @param {Boolean} bool The flag to enable or not. + */ + this.display = function(bool){ + if ( gui ) { + gui.display(bool); + } + }; + +}; // ZoomAndPan class + +/** + * Help for this tool. + * @return {Object} The help content. + */ +dwv.tool.ZoomAndPan.prototype.getHelp = function() +{ + return { + "title": dwv.i18n("tool.ZoomAndPan.name"), + "brief": dwv.i18n("tool.ZoomAndPan.brief"), + "mouse": { + "mouse_wheel": dwv.i18n("tool.ZoomAndPan.mouse_wheel"), + "mouse_drag": dwv.i18n("tool.ZoomAndPan.mouse_drag") + }, + "touch": { + 'twotouch_pinch': dwv.i18n("tool.ZoomAndPan.twotouch_pinch"), + 'touch_drag': dwv.i18n("tool.ZoomAndPan.touch_drag") + } + }; +}; + +/** + * Initialise the tool. + */ +dwv.tool.ZoomAndPan.prototype.init = function() { + return true; +}; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.browser = dwv.browser || {}; + +/** + * Local function to ask Modernizr if a property is supported. + * @parma {String} property The property to test. + */ +function askModernizr( property ) { + if ( typeof dwv.Modernizr === "undefined" ) { + dwv.ModernizrInit(window, document); + } + var props = property.split('.'); + var prop = dwv.Modernizr; + for ( var i = 0; i < props.length; ++i ) { + prop = prop[props[i]]; + } + return prop; +} + +/** + * Browser check for the FileAPI. + * Assume support for Safari5. + */ +dwv.browser.hasFileApi = function() +{ + // regular test does not work on Safari 5 + var isSafari5 = (navigator.appVersion.indexOf("Safari") !== -1) && + (navigator.appVersion.indexOf("Chrome") === -1) && + ( (navigator.appVersion.indexOf("5.0.") !== -1) || + (navigator.appVersion.indexOf("5.1.") !== -1) ); + if( isSafari5 ) + { + console.warn("Assuming FileAPI support for Safari5..."); + return true; + } + // regular test + return askModernizr("filereader"); +}; + +/** + * Browser check for the XMLHttpRequest. + */ +dwv.browser.hasXmlHttpRequest = function() +{ + return askModernizr("xhrresponsetype") && + askModernizr("xhrresponsetypearraybuffer") && + askModernizr("xhrresponsetypetext") && + "XMLHttpRequest" in window && "withCredentials" in new XMLHttpRequest(); +}; + +/** + * Browser check for typed array. + */ +dwv.browser.hasTypedArray = function() +{ + return askModernizr("dataview") && askModernizr("typedarrays"); +}; + +/** + * Browser check for input with type='color'. + * Missing in IE and Safari. + */ +dwv.browser.hasInputColor = function() +{ + return askModernizr("inputtypes.color"); +}; + +/** + * Browser check for input with type='files' and webkitdirectory flag. + * Missing in IE and Safari. + */ +dwv.browser.hasInputDirectory = function() +{ + return askModernizr("fileinputdirectory"); +}; + + +//only check at startup (since we propose a replacement) +dwv.browser._hasTypedArraySlice = (typeof Uint8Array.prototype.slice !== "undefined"); + +/** + * Browser check for typed array slice method. + * Missing in Internet Explorer 11. + */ +dwv.browser.hasTypedArraySlice = function() +{ + return dwv.browser._hasTypedArraySlice; +}; + +// only check at startup (since we propose a replacement) +dwv.browser._hasFloat64Array = ("Float64Array" in window); + +/** + * Browser check for Float64Array array. + * Missing in PhantomJS 1.9.20 (on Travis). + */ +dwv.browser.hasFloat64Array = function() +{ + return dwv.browser._hasFloat64Array; +}; + +//only check at startup (since we propose a replacement) +dwv.browser._hasClampedArray = ("Uint8ClampedArray" in window); + +/** + * Browser check for clamped array. + * Missing in: + * - Safari 5.1.7 for Windows + * - PhantomJS 1.9.20 (on Travis). + */ +dwv.browser.hasClampedArray = function() +{ + return dwv.browser._hasClampedArray; +}; + +/** + * Browser checks to see if it can run dwv. Throws an error if not. + * Silently replaces basic functions. + */ +dwv.browser.check = function() +{ + + // Required -------------- + + var appnorun = "The application cannot be run."; + var message = ""; + // Check for the File API support + if( !dwv.browser.hasFileApi() ) { + message = "The File APIs are not supported in this browser. "; + alert(message+appnorun); + throw new Error(message); + } + // Check for XMLHttpRequest + if( !dwv.browser.hasXmlHttpRequest() ) { + message = "The XMLHttpRequest is not supported in this browser. "; + alert(message+appnorun); + throw new Error(message); + } + // Check typed array + if( !dwv.browser.hasTypedArray() ) { + message = "The Typed arrays are not supported in this browser. "; + alert(message+appnorun); + throw new Error(message); + } + + // Replaced if not present ------------ + + // Check typed array slice + if( !dwv.browser.hasTypedArraySlice() ) { + // silent fail with warning + console.warn("The TypedArray.slice method is not supported in this browser. This may impair performance. "); + // basic Uint16Array implementation + Uint16Array.prototype.slice = function (begin, end) { + var size = end - begin; + var cloned = new Uint16Array(size); + for (var i = 0; i < size; i++) { + cloned[i] = this[begin + i]; + } + return cloned; + }; + // basic Int16Array implementation + Int16Array.prototype.slice = function (begin, end) { + var size = end - begin; + var cloned = new Int16Array(size); + for (var i = 0; i < size; i++) { + cloned[i] = this[begin + i]; + } + return cloned; + }; + // basic Uint8Array implementation + Uint8Array.prototype.slice = function (begin, end) { + var size = end - begin; + var cloned = new Uint8Array(size); + for (var i = 0; i < size; i++) { + cloned[i] = this[begin + i]; + } + return cloned; + }; + // basic Int8Array implementation + Int8Array.prototype.slice = function (begin, end) { + var size = end - begin; + var cloned = new Int8Array(size); + for (var i = 0; i < size; i++) { + cloned[i] = this[begin + i]; + } + return cloned; + }; + } + // check clamped array + if( !dwv.browser.hasClampedArray() ) { + // silent fail with warning + console.warn("The Uint8ClampedArray is not supported in this browser. This may impair performance. "); + // Use Uint8Array instead... Not good + // TODO Find better replacement! + window.Uint8ClampedArray = window.Uint8Array; + } + // check Float64 array + if( !dwv.browser.hasFloat64Array() ) { + // silent fail with warning + console.warn("The Float64Array is not supported in this browser. This may impair performance. "); + // Use Float32Array instead... Not good + // TODO Find better replacement! + window.Float64Array = window.Float32Array; + } + + // check string startsWith + if ( !String.prototype.startsWith ) { + String.prototype.startsWith = function (search, pos) { + return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search; + }; + } +}; + +// namespaces +var dwv = dwv || {}; +// external +var i18next = i18next || {}; +var i18nextXHRBackend = i18nextXHRBackend || {}; +var i18nextBrowserLanguageDetector = i18nextBrowserLanguageDetector || {}; + +// This is mainly a wrapper around the i18next object. +// see its API: http://i18next.com/docs/api/ + +// global locales path +dwv.i18nLocalesPath = null; + +/** + * Initialise i18n. + * @param {String} language The language to translate to. Defaults to 'auto' and + * gets the language from the browser. + * @param {String} localesPath Path to the locales directory. + * @external i18next + * @external i18nextXHRBackend + * @external i18nextBrowserLanguageDetector + */ +dwv.i18nInitialise = function (language, localesPath) +{ + var lng = (typeof language === "undefined") ? "auto" : language; + var lpath = (typeof localesPath === "undefined") ? "../.." : localesPath; + // store as global + dwv.i18nLocalesPath = lpath; + // i18n options: default 'en' language and + // only load language, not specialised (for ex en-GB) + var options = { + fallbackLng: "en", + load: "languageOnly", + backend: { loadPath: lpath + "/locales/{{lng}}/{{ns}}.json" } + }; + // use the XHR backend to get translation files + var i18n = i18next.use(i18nextXHRBackend); + // use browser language or the specified one + if (lng === "auto") { + i18n.use(i18nextBrowserLanguageDetector); + } + else { + options.lng = lng; + } + // init i18n: will be ready when the 'loaded' event is fired + i18n.init(options); +}; + +/** + * Initialise i18n with recources as input. + * @param {String} language The language to translate to. Defaults to 'auto' and + * gets the language from the browser. + * @param {Object} resources Languages provided as object. + * @external i18next + * @external i18nextBrowserLanguageDetector + */ +dwv.i18nInitialiseWithResources = function (language, resources) +{ + var lng = (typeof language === "undefined") ? "auto" : language; + // i18n options: default 'en' language and + // only load language, not specialised (for ex en-GB) + var options = { + fallbackLng: "en", + load: "languageOnly", + resources: resources + }; + // use browser language or the specified one + // init i18n: will be ready when the 'loaded' event is fired + if (lng === "auto") { + var i18n = i18next.use(i18nextBrowserLanguageDetector); + i18n.init(options); + } + else { + options.lng = lng; + i18next.init(options); + } +}; + +/** + * Handle i18n 'initialized' event. + * @param {Object} callback The callback function to call when i18n is initialised. + * It can take one argument that will be replaced with the i18n options. + * @external i18next + */ +dwv.i18nOnInitialised = function (callback) { + i18next.on('initialized', callback); +}; + +/** + * Stop handling i18n load event. + * @external i18next + */ +dwv.i18nOffInitialised = function () { + i18next.off('initialized'); +}; + +/** + * Handle i18n failed load event. + * @param {Object} callback The callback function to call when i18n is loaded. + * It can take three arguments: lng, ns and msg. + * @external i18next + */ +dwv.i18nOnFailedLoad = function (callback) { + i18next.on('failedLoading', callback); +}; + +/** + * Stop handling i18n failed load event. + * @external i18next + */ +dwv.i18nOffFailedLoad = function () { + i18next.off('failedLoading'); +}; + +/** + * Get the translated text. + * @param {String} key The key to the text entry. + * @param {Object} options The translation options such as plural, context... + * @external i18next + */ +dwv.i18n = function (key, options) { + return i18next.t(key, options); +}; + +/** + * Check the existence of a translation. + * @param {String} key The key to the text entry. + * @param {Object} options The translation options such as plural, context... + * @external i18next + */ +dwv.i18nExists = function (key, options) { + return i18next.exists(key, options); +}; + +/** + * Translate all data-i18n tags in the current html page. If an html tag defines the + * data-i18n attribute, its value will be used as key to find its corresponding text + * and will replace the content of the html tag. + */ +dwv.i18nPage = function () { + // get all elements + var elements = document.getElementsByTagName("*"); + // if the element defines data-i18n, replace its content with the tranlation + for (var i = 0; i < elements.length; ++i) { + if (typeof elements[i].dataset.i18n !== "undefined") { + elements[i].innerHTML = dwv.i18n(elements[i].dataset.i18n); + } + } +}; + +/** + * Get the current locale resource path. + * Warning: to be used once i18next is initialised. + * @return {String} The path to the locale resource. + */ +dwv.i18nGetLocalePath = function (filename) { + var lng = i18next.language.substr(0, 2); + return dwv.i18nLocalesPath + + "/locales/" + lng + "/" + filename; +}; + +/** + * Get the current locale resource path. + * Warning: to be used once i18next is initialised. + * @return {String} The path to the locale resource. + */ +dwv.i18nGetFallbackLocalePath = function (filename) { + var lng = i18next.languages[i18next.languages.length-1].substr(0, 2); + return dwv.i18nLocalesPath + + "/locales/" + lng + "/" + filename; +}; + +// namespaces +var dwv = dwv || {}; +dwv.utils = dwv.utils || {}; + +/** + * ListenerHandler class: handles add/removing and firing listeners. + * @constructor + */ +dwv.utils.ListenerHandler = function () +{ + /** + * listeners. + * @private + */ + var listeners = {}; + + /** + * Add an event listener. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type, + * will be called with the fired event. + */ + this.add = function (type, callback) + { + // create array if not present + if ( typeof listeners[type] === "undefined" ) { + listeners[type] = []; + } + // add callback to listeners array + listeners[type].push(callback); + }; + + /** + * Remove an event listener. + * @param {String} type The event type. + * @param {Object} callback The method associated with the provided event type. + */ + this.remove = function (type, callback) + { + // check if the type is present + if( typeof listeners[type] === "undefined" ) { + return; + } + // remove from listeners array + for ( var i = 0; i < listeners[type].length; ++i ) { + if ( listeners[type][i] === callback ) { + listeners[type].splice(i,1); + } + } + }; + + /** + * Fire an event: call all associated listeners with the input event object. + * @param {Object} event The event to fire. + */ + this.fireEvent = function (event) + { + // check if they are listeners for the event type + if ( typeof listeners[event.type] === "undefined" ) { + return; + } + // fire events + for ( var i=0; i < listeners[event.type].length; ++i ) { + listeners[event.type][i](event); + } + }; +}; + +// namespaces +var dwv = dwv || {}; + +/*! + * modernizr v3.6.0 + * Build https://modernizr.com/download?-dataview-directory-filereader-inputtypes-typedarrays-xhrresponsetype-xhrresponsetypearraybuffer-xhrresponsetypejson-xhrresponsetypetext-dontmin + * + * Copyright (c) + * Faruk Ates + * Paul Irish + * Alex Sexton + * Ryan Seddon + * Patrick Kettner + * Stu Cox + * Richard Herrera + + * MIT License + */ + +/* + * Modernizr tests which native CSS3 and HTML5 features are available in the + * current UA and makes the results available to you in two ways: as properties on + * a global `Modernizr` object, and as classes on the `` element. This + * information allows you to progressively enhance your pages with a granular level + * of control over the experience. +*/ + +dwv.ModernizrInit = function (window, document, undefined) { +//;(function(window, document, undefined){ + var tests = []; + + + /** + * + * ModernizrProto is the constructor for Modernizr + * + * @class + * @access public + */ + + var ModernizrProto = { + // The current version, dummy + _version: '3.6.0', + + // Any settings that don't work as separate modules + // can go in here as configuration. + _config: { + 'classPrefix': '', + 'enableClasses': true, + 'enableJSClass': true, + 'usePrefixes': true + }, + + // Queue of tests + _q: [], + + // Stub these for people who are listening + on: function(test, cb) { + // I don't really think people should do this, but we can + // safe guard it a bit. + // -- NOTE:: this gets WAY overridden in src/addTest for actual async tests. + // This is in case people listen to synchronous tests. I would leave it out, + // but the code to *disallow* sync tests in the real version of this + // function is actually larger than this. + var self = this; + setTimeout(function() { + cb(self[test]); + }, 0); + }, + + addTest: function(name, fn, options) { + tests.push({name: name, fn: fn, options: options}); + }, + + addAsyncTest: function(fn) { + tests.push({name: null, fn: fn}); + } + }; + + + + // Fake some of Object.create so we can force non test results to be non "own" properties. + var Modernizr = function() {}; + Modernizr.prototype = ModernizrProto; + + // Leak modernizr globally when you `require` it rather than force it here. + // Overwrite name so constructor name is nicer :D + Modernizr = new Modernizr(); + + +/*! +{ + "name": "DataView", + "property": "dataview", + "authors": ["Addy Osmani"], + "builderAliases": ["dataview_api"], + "notes": [{ + "name": "MDN documentation", + "href": "https://developer.mozilla.org/en/JavaScript_typed_arrays/DataView" + }], + "polyfills": ["jdataview"] +} +!*/ +/* DOC +Detects support for the DataView interface for reading data from an ArrayBuffer as part of the Typed Array spec. +*/ + + Modernizr.addTest('dataview', (typeof DataView !== 'undefined' && 'getFloat64' in DataView.prototype)); + +/*! +{ + "name": "Typed arrays", + "property": "typedarrays", + "caniuse": "typedarrays", + "tags": ["js"], + "authors": ["Stanley Stuart (@fivetanley)"], + "notes": [{ + "name": "MDN documentation", + "href": "https://developer.mozilla.org/en-US/docs/JavaScript_typed_arrays" + },{ + "name": "Kronos spec", + "href": "https://www.khronos.org/registry/typedarray/specs/latest/" + }], + "polyfills": ["joshuabell-polyfill"] +} +!*/ +/* DOC +Detects support for native binary data manipulation via Typed Arrays in JavaScript. + +Does not check for DataView support; use `Modernizr.dataview` for that. +*/ + + // Should fail in: + // Internet Explorer <= 9 + // Firefox <= 3.6 + // Chrome <= 6.0 + // iOS Safari < 4.2 + // Safari < 5.1 + // Opera < 11.6 + // Opera Mini, <= 7.0 + // Android Browser < 4.0 + // Blackberry Browser < 10.0 + + Modernizr.addTest('typedarrays', 'ArrayBuffer' in window); + +/*! +{ + "name": "File API", + "property": "filereader", + "caniuse": "fileapi", + "notes": [{ + "name": "W3C Working Draft", + "href": "https://www.w3.org/TR/FileAPI/" + }], + "tags": ["file"], + "builderAliases": ["file_api"], + "knownBugs": ["Will fail in Safari 5 due to its lack of support for the standards defined FileReader object"] +} +!*/ +/* DOC +`filereader` tests for the File API specification + +Tests for objects specific to the File API W3C specification without +being redundant (don't bother testing for Blob since it is assumed +to be the File object's prototype.) +*/ + + Modernizr.addTest('filereader', !!(window.File && window.FileList && window.FileReader)); + +/*! +{ + "name": "XHR responseType", + "property": "xhrresponsetype", + "tags": ["network"], + "notes": [{ + "name": "XMLHttpRequest Living Standard", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/* DOC +Tests for XMLHttpRequest xhr.responseType. +*/ + + Modernizr.addTest('xhrresponsetype', (function() { + if (typeof XMLHttpRequest == 'undefined') { + return false; + } + var xhr = new XMLHttpRequest(); + xhr.open('get', '/', true); + return 'response' in xhr; + }())); + + + var classes = []; + + + /** + * is returns a boolean if the typeof an obj is exactly type. + * + * @access private + * @function is + * @param {*} obj - A thing we want to check the type of + * @param {string} type - A string to compare the typeof against + * @returns {boolean} + */ + + function is(obj, type) { + return typeof obj === type; + } + ; + + /** + * Run through all tests and detect their support in the current UA. + * + * @access private + */ + + function testRunner() { + var featureNames; + var feature; + var aliasIdx; + var result; + var nameIdx; + var featureName; + var featureNameSplit; + + for (var featureIdx in tests) { + if (tests.hasOwnProperty(featureIdx)) { + featureNames = []; + feature = tests[featureIdx]; + // run the test, throw the return value into the Modernizr, + // then based on that boolean, define an appropriate className + // and push it into an array of classes we'll join later. + // + // If there is no name, it's an 'async' test that is run, + // but not directly added to the object. That should + // be done with a post-run addTest call. + if (feature.name) { + featureNames.push(feature.name.toLowerCase()); + + if (feature.options && feature.options.aliases && feature.options.aliases.length) { + // Add all the aliases into the names list + for (aliasIdx = 0; aliasIdx < feature.options.aliases.length; aliasIdx++) { + featureNames.push(feature.options.aliases[aliasIdx].toLowerCase()); + } + } + } + + // Run the test, or use the raw value if it's not a function + result = is(feature.fn, 'function') ? feature.fn() : feature.fn; + + + // Set each of the names on the Modernizr object + for (nameIdx = 0; nameIdx < featureNames.length; nameIdx++) { + featureName = featureNames[nameIdx]; + // Support dot properties as sub tests. We don't do checking to make sure + // that the implied parent tests have been added. You must call them in + // order (either in the test, or make the parent test a dependency). + // + // Cap it to TWO to make the logic simple and because who needs that kind of subtesting + // hashtag famous last words + featureNameSplit = featureName.split('.'); + + if (featureNameSplit.length === 1) { + Modernizr[featureNameSplit[0]] = result; + } else { + // cast to a Boolean, if not one already + if (Modernizr[featureNameSplit[0]] && !(Modernizr[featureNameSplit[0]] instanceof Boolean)) { + Modernizr[featureNameSplit[0]] = new Boolean(Modernizr[featureNameSplit[0]]); + } + + Modernizr[featureNameSplit[0]][featureNameSplit[1]] = result; + } + + classes.push((result ? '' : 'no-') + featureNameSplit.join('-')); + } + } + } + } + ; + + /** + * docElement is a convenience wrapper to grab the root element of the document + * + * @access private + * @returns {HTMLElement|SVGElement} The root element of the document + */ + + var docElement = document.documentElement; + + + /** + * http://mathiasbynens.be/notes/xhr-responsetype-json#comment-4 + * + * @access private + * @function testXhrType + * @param {string} type - String name of the XHR type you want to detect + * @returns {boolean} + * @author Mathias Bynens + */ + + /* istanbul ignore next */ + var testXhrType = function(type) { + if (typeof XMLHttpRequest == 'undefined') { + return false; + } + var xhr = new XMLHttpRequest(); + xhr.open('get', '/', true); + try { + xhr.responseType = type; + } catch (error) { + return false; + } + return 'response' in xhr && xhr.responseType == type; + }; + + +/*! +{ + "name": "XHR responseType='arraybuffer'", + "property": "xhrresponsetypearraybuffer", + "tags": ["network"], + "notes": [{ + "name": "XMLHttpRequest Living Standard", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/* DOC +Tests for XMLHttpRequest xhr.responseType='arraybuffer'. +*/ + + Modernizr.addTest('xhrresponsetypearraybuffer', testXhrType('arraybuffer')); + +/*! +{ + "name": "XHR responseType='json'", + "property": "xhrresponsetypejson", + "tags": ["network"], + "notes": [{ + "name": "XMLHttpRequest Living Standard", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + },{ + "name": "Explanation of xhr.responseType='json'", + "href": "https://mathiasbynens.be/notes/xhr-responsetype-json" + }] +} +!*/ +/* DOC +Tests for XMLHttpRequest xhr.responseType='json'. +*/ + + Modernizr.addTest('xhrresponsetypejson', testXhrType('json')); + +/*! +{ + "name": "XHR responseType='text'", + "property": "xhrresponsetypetext", + "tags": ["network"], + "notes": [{ + "name": "XMLHttpRequest Living Standard", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/* DOC +Tests for XMLHttpRequest xhr.responseType='text'. +*/ + + Modernizr.addTest('xhrresponsetypetext', testXhrType('text')); + + + /** + * A convenience helper to check if the document we are running in is an SVG document + * + * @access private + * @returns {boolean} + */ + + var isSVG = docElement.nodeName.toLowerCase() === 'svg'; + + + /** + * createElement is a convenience wrapper around document.createElement. Since we + * use createElement all over the place, this allows for (slightly) smaller code + * as well as abstracting away issues with creating elements in contexts other than + * HTML documents (e.g. SVG documents). + * + * @access private + * @function createElement + * @returns {HTMLElement|SVGElement} An HTML or SVG element + */ + + function createElement() { + if (typeof document.createElement !== 'function') { + // This is the case in IE7, where the type of createElement is "object". + // For this reason, we cannot call apply() as Object is not a Function. + return document.createElement(arguments[0]); + } else if (isSVG) { + return document.createElementNS.call(document, 'http://www.w3.org/2000/svg', arguments[0]); + } else { + return document.createElement.apply(document, arguments); + } + } + + ; + + /** + * since we have a fairly large number of input tests that don't mutate the input + * we create a single element that can be shared with all of those tests for a + * minor perf boost + * + * @access private + * @returns {HTMLInputElement} + */ + var inputElem = createElement('input'); + +/*! +{ + "name": "Form input types", + "property": "inputtypes", + "caniuse": "forms", + "tags": ["forms"], + "authors": ["Mike Taylor"], + "polyfills": [ + "jquerytools", + "webshims", + "h5f", + "webforms2", + "nwxforms", + "fdslider", + "html5slider", + "galleryhtml5forms", + "jscolor", + "html5formshim", + "selectedoptionsjs", + "formvalidationjs" + ] +} +!*/ +/* DOC +Detects support for HTML5 form input types and exposes Boolean subproperties with the results: + +```javascript +Modernizr.inputtypes.color +Modernizr.inputtypes.date +Modernizr.inputtypes.datetime +Modernizr.inputtypes['datetime-local'] +Modernizr.inputtypes.email +Modernizr.inputtypes.month +Modernizr.inputtypes.number +Modernizr.inputtypes.range +Modernizr.inputtypes.search +Modernizr.inputtypes.tel +Modernizr.inputtypes.time +Modernizr.inputtypes.url +Modernizr.inputtypes.week +``` +*/ + + // Run through HTML5's new input types to see if the UA understands any. + // This is put behind the tests runloop because it doesn't return a + // true/false like all the other tests; instead, it returns an object + // containing each input type with its corresponding true/false value + + // Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/ + var inputtypes = 'search tel url email datetime date month week time datetime-local number range color'.split(' '); + var inputs = {}; + + Modernizr.inputtypes = (function(props) { + var len = props.length; + var smile = '1)'; + var inputElemType; + var defaultView; + var bool; + + for (var i = 0; i < len; i++) { + + inputElem.setAttribute('type', inputElemType = props[i]); + bool = inputElem.type !== 'text' && 'style' in inputElem; + + // We first check to see if the type we give it sticks.. + // If the type does, we feed it a textual value, which shouldn't be valid. + // If the value doesn't stick, we know there's input sanitization which infers a custom UI + if (bool) { + + inputElem.value = smile; + inputElem.style.cssText = 'position:absolute;visibility:hidden;'; + + if (/^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined) { + + docElement.appendChild(inputElem); + defaultView = document.defaultView; + + // Safari 2-4 allows the smiley as a value, despite making a slider + bool = defaultView.getComputedStyle && + defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' && + // Mobile android web browser has false positive, so must + // check the height to see if the widget is actually there. + (inputElem.offsetHeight !== 0); + + docElement.removeChild(inputElem); + + } else if (/^(search|tel)$/.test(inputElemType)) { + // Spec doesn't define any special parsing or detectable UI + // behaviors so we pass these through as true + + // Interestingly, opera fails the earlier test, so it doesn't + // even make it here. + + } else if (/^(url|email)$/.test(inputElemType)) { + // Real url and email support comes with prebaked validation. + bool = inputElem.checkValidity && inputElem.checkValidity() === false; + + } else { + // If the upgraded input compontent rejects the :) text, we got a winner + bool = inputElem.value != smile; + } + } + + inputs[ props[i] ] = !!bool; + } + return inputs; + })(inputtypes); + + + /** + * If the browsers follow the spec, then they would expose vendor-specific styles as: + * elem.style.WebkitBorderRadius + * instead of something like the following (which is technically incorrect): + * elem.style.webkitBorderRadius + + * WebKit ghosts their properties in lowercase but Opera & Moz do not. + * Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+ + * erik.eae.net/archives/2008/03/10/21.48.10/ + + * More here: github.com/Modernizr/Modernizr/issues/issue/21 + * + * @access private + * @returns {string} The string representing the vendor-specific style properties + */ + + var omPrefixes = 'Moz O ms Webkit'; + + + /** + * List of JavaScript DOM values used for tests + * + * @memberof Modernizr + * @name Modernizr._domPrefixes + * @optionName Modernizr._domPrefixes + * @optionProp domPrefixes + * @access public + * @example + * + * Modernizr._domPrefixes is exactly the same as [_prefixes](#modernizr-_prefixes), but rather + * than kebab-case properties, all properties are their Capitalized variant + * + * ```js + * Modernizr._domPrefixes === [ "Moz", "O", "ms", "Webkit" ]; + * ``` + */ + + var domPrefixes = (ModernizrProto._config.usePrefixes ? omPrefixes.toLowerCase().split(' ') : []); + ModernizrProto._domPrefixes = domPrefixes; + +/*! +{ + "name": "input[directory] Attribute", + "property": "directory", + "authors": ["silverwind"], + "tags": ["file", "input", "attribute"] +} +!*/ +/* DOC +When used on an ``, the `directory` attribute instructs +the user agent to present a directory selection dialog instead of the usual +file selection dialog. +*/ + + Modernizr.addTest('fileinputdirectory', function() { + var elem = createElement('input'), dir = 'directory'; + elem.type = 'file'; + if (dir in elem) { + return true; + } else { + for (var i = 0, len = domPrefixes.length; i < len; i++) { + if (domPrefixes[i] + dir in elem) { + return true; + } + } + } + return false; + }); + + + // Run each test + testRunner(); + + delete ModernizrProto.addTest; + delete ModernizrProto.addAsyncTest; + + // Run the things that are supposed to run after the tests + for (var i = 0; i < Modernizr._q.length; i++) { + Modernizr._q[i](); + } + + // Leak Modernizr namespace + //window.Modernizr = Modernizr; + dwv.Modernizr = Modernizr; + + +; + +//})(window, document); +}; + +// namespaces +var dwv = dwv || {}; +dwv.utils = dwv.utils || {}; + +/** + * Multiple progresses handler. + * Stores a multi dimensional list of progresses to allow to + * calculate a global progress. + * @param {Function} callback The function to pass the global progress to. + */ +dwv.utils.MultiProgressHandler = function (callback) +{ + // closure to self + var self = this; + + /** + * List of progresses. + * @private + * @type Array + */ + var progresses = []; + + /** + * Number of dimensions. + * @private + * @type Number + */ + var numberOfDimensions = 2; + + /** + * Set the number of dimensions. + * @param {Number} num The number. + */ + this.setNumberOfDimensions = function (num) { + numberOfDimensions = num; + }; + + /** + * Set the number of data to load. + * @param {Number} n The number of data to load. + */ + this.setNToLoad = function (n) { + for ( var i = 0; i < n; ++i ) { + progresses[i] = []; + for ( var j = 0; j < numberOfDimensions; ++j ) { + progresses[i][j] = 0; + } + } + }; + + /** + * Handle a load progress. + * @param {Object} event The progress event. + */ + this.onprogress = function (event) { + // check event + if ( !event.lengthComputable ) { + return; + } + if ( typeof event.subindex === "undefined" ) { + return; + } + if ( typeof event.index === "undefined" ) { + return; + } + // calculate percent + var percent = (event.loaded * 100) / event.total; + // set percent for index + progresses[event.index][event.subindex] = percent; + + // call callback + callback({'type': event.type, 'lengthComputable': true, + 'loaded': getGlobalPercent(), 'total': 100}); + }; + + /** + * Get the global load percent including the provided one. + * @return {Number} The accumulated percentage. + */ + function getGlobalPercent() { + var sum = 0; + var lenprog = progresses.length; + for ( var i = 0; i < lenprog; ++i ) { + for ( var j = 0; j < numberOfDimensions; ++j ) { + sum += progresses[i][j]; + } + } + return Math.round( sum / (lenprog * numberOfDimensions) ); + } + + /** + * Create a mono progress event handler. + * @param {Number} index The index of the data. + * @param {Number} subindex The sub-index of the data. + */ + this.getMonoProgressHandler = function (index, subindex) { + return function (event) { + event.index = index; + event.subindex = subindex; + self.onprogress(event); + }; + }; + + /** + * Create a mono loadend event handler: sends a 100% progress. + * @param {Number} index The index of the data. + * @param {Number} subindex The sub-index of the data. + */ + this.getMonoOnLoadEndHandler = function (index, subindex) { + return function () { + self.onprogress({'type': 'load-progress', 'lengthComputable': true, + 'loaded': 100, 'total': 100, + 'index': index, 'subindex': subindex}); + }; + }; + + /** + * Create a mono progress event handler with an undefined index. + * Warning: The caller handles the progress index. + * @param {Number} subindex The sub-index of the data. + */ + this.getUndefinedMonoProgressHandler = function (subindex) { + return function (event) { + event.subindex = subindex; + self.onprogress(event); + }; + }; +}; + +// namespaces +var dwv = dwv || {}; +/** @namespace */ +dwv.utils = dwv.utils || {}; + +/** + * Capitalise the first letter of a string. + * @param {String} string The string to capitalise the first letter. + * @return {String} The new string. + */ +dwv.utils.capitaliseFirstLetter = function (string) +{ + var res = string; + if ( string ) { + res = string.charAt(0).toUpperCase() + string.slice(1); + } + return res; +}; + +/** + * Split key/value string: + * key0=val00&key0=val01&key1=val10 returns + * { key0 : [val00, val01], key1 : val1 } + * @param {String} inputStr The string to split. + * @return {Object} The split string. + */ +dwv.utils.splitKeyValueString = function (inputStr) +{ + // result + var result = {}; + // check input string + if ( inputStr ) { + // split key/value pairs + var pairs = inputStr.split('&'); + for ( var i = 0; i < pairs.length; ++i ) + { + var pair = pairs[i].split('='); + // if the key does not exist, create it + if ( !result[pair[0]] ) + { + result[pair[0]] = pair[1]; + } + else + { + // make it an array + if ( !( result[pair[0]] instanceof Array ) ) { + result[pair[0]] = [result[pair[0]]]; + } + result[pair[0]].push(pair[1]); + } + } + } + return result; +}; + +/** + * Replace flags in a input string. Flags are keywords surrounded with curly + * braces. + * @param {String} inputStr The input string. + * @param {Object} values A object of {value, unit}. + * @example + * var values = {"length": { "value": 33, "unit": "cm" } }; + * var str = "The length is: {length}."; + * var res = dwv.utils.replaceFlags(str, values); // "The length is: 33 cm." + * @return {String} The result string. + */ +dwv.utils.replaceFlags = function (inputStr, values) +{ + var res = ""; + // check input string + if (inputStr === null || typeof inputStr === "undefined") { + return res; + } + res = inputStr; + // check values + if (values === null || typeof values === "undefined") { + return res; + } + // loop through values keys + var keys = Object.keys(values); + for (var i = 0; i < keys.length; ++i) { + var valueObj = values[keys[i]]; + if ( valueObj !== null && typeof valueObj !== "undefined" && + valueObj.value !== null && typeof valueObj.value !== "undefined") { + // value string + var valueStr = valueObj.value.toPrecision(4); + // add unit if available + // space or no space? Yes apart from degree... + // check: https://en.wikipedia.org/wiki/Space_(punctuation)#Spaces_and_unit_symbols + if (valueObj.unit !== null && typeof valueObj.unit !== "undefined" && + valueObj.unit.length !== 0) { + if (valueObj.unit !== "degree") { + valueStr += " "; + } + valueStr += valueObj.unit; + } + // flag to replace + var flag = '{' + keys[i] + '}'; + // replace + res = res.replace(flag, valueStr); + } + } + // return + return res; +}; + +/** + * Replace flags in a input string. Flags are keywords surrounded with curly + * braces. + * @param {String} inputStr The input string. + * @param {Array} values An array of strings. + * @example + * var values = ["a", "b"]; + * var str = "The length is: {v0}. The size is: {v1}"; + * var res = dwv.utils.replaceFlags2(str, values); // "The length is: a. The size is: b" + * @return {String} The result string. + */ +dwv.utils.replaceFlags2 = function (inputStr, values) +{ + var res = inputStr; + for ( var j = 0; j < values.length; ++j ) { + res = res.replace("{v"+j+"}", values[j]); + } + return res; +}; + +dwv.utils.createDefaultReplaceFormat = function (values) +{ + var res = ""; + for ( var j = 0; j < values.length; ++j ) { + if ( j !== 0 ) { + res += ", "; + } + res += "{v"+j+"}"; + } + return res; +}; + +// namespaces +var dwv = dwv || {}; +dwv.utils = dwv.utils || {}; + +/** + * Thread Pool. + * Highly inspired from {@link http://www.smartjava.org/content/html5-easily-parallelize-jobs-using-web-workers-and-threadpool}. + * @constructor + * @param {Number} poolSize The size of the pool. + */ +dwv.utils.ThreadPool = function (poolSize) { + + // task queue + var taskQueue = []; + // lsit of available threads + var freeThreads = []; + // list of running threads (unsed in abort) + var runningThreads = []; + + /** + * Initialise. + */ + this.init = function () { + // create 'poolSize' number of worker threads + for (var i = 0; i < poolSize; ++i) { + freeThreads.push(new dwv.utils.WorkerThread(this)); + } + }; + + /** + * Add a worker task to the queue. + * Will be run when a thread is made available. + * @return {Object} workerTask The task to add. + */ + this.addWorkerTask = function (workerTask) { + if (freeThreads.length > 0) { + // get the first free worker thread + var workerThread = freeThreads.shift(); + // run the input task + workerThread.run(workerTask); + // add the thread to the runnning list + runningThreads.push(workerThread); + } else { + // no free thread, add task to queue + taskQueue.push(workerTask); + } + }; + + /** + * Abort all threads. + */ + this.abort = function () { + // clear tasks + taskQueue = []; + // cancel running workers + for (var i = 0; i < runningThreads.length; ++i) { + runningThreads[i].stop(); + } + runningThreads = []; + // re-init + this.init(); + }; + + /** + * Free a worker thread. + * @param {Object} workerThread The thread to free. + */ + this.freeWorkerThread = function (workerThread) { + // send worker end + this.onworkerend(); + + if (taskQueue.length > 0) { + // get waiting task + var workerTask = taskQueue.shift(); + // use input thread to run the waiting task + workerThread.run(workerTask); + } else { + // no task to run, add to free list + freeThreads.push(workerThread); + // remove from running list + for ( var i = 0; i < runningThreads.length; ++i ) { + if ( runningThreads[i].getId() === workerThread.getId() ) { + runningThreads.splice(i, 1); + } + } + // the work is done when the queue is back to its initial size + if ( freeThreads.length === poolSize ) { + this.onpoolworkend(); + } + } + }; +}; + +/** + * Handle a pool work end event. + * Default does nothing. + */ +dwv.utils.ThreadPool.prototype.onpoolworkend = function () {}; +/** + * Handle a pool worker end event. + * Default does nothing. + */ +dwv.utils.ThreadPool.prototype.onworkerend = function () {}; + +/** + * Worker thread. + * @external Worker + * @constructor + * @param {Object} parentPool The parent pool. + */ +dwv.utils.WorkerThread = function (parentPool) { + // closure to self + var self = this; + + // thread ID + var id = Math.random().toString(36).substring(2, 15); + + // running task + var runningTask = {}; + // worker used to run task + var worker; + + /** + * Get the thread ID. + * @return {String} The thread ID (alphanumeric). + */ + this.getId = function () { + return id; + }; + + /** + * Run a worker task + * @param {Object} workerTask The task to run. + */ + this.run = function (workerTask) { + // store task + runningTask = workerTask; + // create a new web worker + if (runningTask.script !== null) { + worker = new Worker(runningTask.script); + worker.addEventListener('message', ontaskend, false); + // launch the worker + worker.postMessage(runningTask.startMessage); + } + }; + + /** + * Stop a run and free the thread. + */ + this.stop = function () { + // stop the worker + worker.terminate(); + // tell the parent pool this thread is free + parentPool.freeWorkerThread(this); + }; + + /** + * Handle once the task is done. + * For now assume we only get a single callback from a worker + * which also indicates the end of this worker. + * @param {Object} event The callback event. + */ + function ontaskend(event) { + // pass to original callback + runningTask.callback(event); + // stop the worker and free the thread + self.stop(); + } + +}; + +/** + * Worker task. + * @constructor + * @param {String} script The worker script. + * @param {Function} callback The worker callback. + * @param {Object} message The data to pass to the worker. + */ +dwv.utils.WorkerTask = function (script, callback, message) { + // worker script + this.script = script; + // worker callback + this.callback = callback; + // worker start message + this.startMessage = message; +}; + +// namespaces +var dwv = dwv || {}; +dwv.utils = dwv.utils || {}; +/** @namespace */ +dwv.utils.base = dwv.utils.base || {}; + +/** + * Split an input URI: + * 'root?key0=val00&key0=val01&key1=val10' returns + * { base : root, query : [ key0 : [val00, val01], key1 : val1 ] } + * Returns an empty object if the input string is not correct (null, empty...) + * or if it is not a query string (no question mark). + * @param {String} inputStr The string to split. + * @return {Object} The split string. + */ +dwv.utils.splitUri = function (uri) +{ + // result + var result = {}; + // check if query string + var sepIndex = null; + if ( uri && (sepIndex = uri.indexOf('?')) !== -1 ) { + // base: before the '?' + result.base = uri.substr(0, sepIndex); + // query : after the '?' and until possible '#' + var hashIndex = uri.indexOf('#'); + if ( hashIndex === -1 ) { + hashIndex = uri.length; + } + var query = uri.substr(sepIndex + 1, (hashIndex - 1 - sepIndex)); + // split key/value pairs of the query + result.query = dwv.utils.splitKeyValueString(query); + } + // return + return result; +}; + +/** + * Get the query part, split into an array, of an input URI. + * The URI scheme is: 'base?query#fragment' + * @param {String } uri The input URI. + * @return {Object} The query part, split into an array, of the input URI. + */ +dwv.utils.getUriQuery = function (uri) +{ + // split + var parts = dwv.utils.splitUri(uri); + // check not empty + if ( Object.keys(parts).length === 0 ) { + return null; + } + // return query + return parts.query; +}; + +/** + * Generic URI query decoder. + * Supports manifest: + * [dwv root]?input=encodeURIComponent('[manifest file]')&type=manifest + * or encoded URI with base and key value/pairs: + * [dwv root]?input=encodeURIComponent([root]?key0=value0&key1=value1) + * @param {String} query The query part to the input URI. + * @param {Function} callback The function to call with the decoded file urls. + */ +dwv.utils.base.decodeQuery = function (query, callback) +{ + // manifest + if ( query.type && query.type === "manifest" ) { + dwv.utils.decodeManifestQuery( query, callback ); + } + // default case: encoded URI with base and key/value pairs + else { + callback( dwv.utils.decodeKeyValueUri( query.input, query.dwvReplaceMode ) ); + } +}; + +/** + * Decode a Key/Value pair URI. If a key is repeated, the result + * be an array of base + each key. + * @param {String} uri The URI to decode. + * @param {String} replaceMode The key replace more. + * replaceMode can be: + * - key (default): keep the key + * - other than key: do not use the key + * 'file' is a special case where the '?' of the query is not kept. + * @return The list of input file urls. + */ +dwv.utils.decodeKeyValueUri = function (uri, replaceMode) +{ + var result = []; + + // repeat key replace mode (default to keep key) + var repeatKeyReplaceMode = "key"; + if ( replaceMode ) { + repeatKeyReplaceMode = replaceMode; + } + + // decode input URI + var queryUri = decodeURIComponent(uri); + // get key/value pairs from input URI + var inputQueryPairs = dwv.utils.splitUri(queryUri); + if ( Object.keys(inputQueryPairs).length === 0 ) + { + result.push(queryUri); + } + else + { + var keys = Object.keys(inputQueryPairs.query); + // find repeat key + var repeatKey = null; + for ( var i = 0; i < keys.length; ++i ) + { + if ( inputQueryPairs.query[keys[i]] instanceof Array ) + { + repeatKey = keys[i]; + break; + } + } + + if ( !repeatKey ) + { + result.push(queryUri); + } + else + { + var repeatList = inputQueryPairs.query[repeatKey]; + // build base uri + var baseUrl = inputQueryPairs.base; + // do not add '?' when the repeatKey is 'file' + // root/path/to/?file=0.jpg&file=1.jpg + if ( repeatKey !== "file" ) { + baseUrl += "?"; + } + var gotOneArg = false; + for ( var j = 0; j < keys.length; ++j ) + { + if ( keys[j] !== repeatKey ) { + if ( gotOneArg ) { + baseUrl += "&"; + } + baseUrl += keys[j] + "=" + inputQueryPairs.query[keys[j]]; + gotOneArg = true; + } + } + // append built urls to result + var url; + for ( var k = 0; k < repeatList.length; ++k ) + { + url = baseUrl; + if ( gotOneArg ) { + url += "&"; + } + if ( repeatKeyReplaceMode === "key" ) { + url += repeatKey + "="; + } + // other than 'key' mode: do nothing + url += repeatList[k]; + result.push(url); + } + } + } + // return + return result; +}; + +/** + * Decode a manifest query. + * @external XMLHttpRequest + * @param {Object} query The manifest query: {input, nslices}, + * with input the input URI and nslices the number of slices. + * @param {Function} The function to call with the decoded urls. + */ +dwv.utils.decodeManifestQuery = function (query, callback) +{ + var uri = ""; + if ( query.input[0] === '/' ) { + uri = window.location.protocol + "//" + window.location.host; + } + // TODO: needs to be decoded (decodeURIComponent? + uri += query.input; + + // handle error + var onError = function (/*event*/) + { + console.warn( "RequestError while receiving manifest: "+this.status ); + }; + + // handle load + var onLoad = function (/*event*/) + { + callback( dwv.utils.decodeManifest(this.responseXML, query.nslices) ); + }; + + var request = new XMLHttpRequest(); + request.open('GET', decodeURIComponent(uri), true); + request.responseType = "document"; + request.onload = onLoad; + request.onerror = onError; + request.send(null); +}; + +/** + * Decode an XML manifest. + * @param {Object} manifest The manifest to decode. + * @param {Number} nslices The number of slices to load. + */ +dwv.utils.decodeManifest = function (manifest, nslices) +{ + var result = []; + // wado url + var wadoElement = manifest.getElementsByTagName("wado_query"); + var wadoURL = wadoElement[0].getAttribute("wadoURL"); + var rootURL = wadoURL + "?requestType=WADO&contentType=application/dicom&"; + // patient list + var patientList = manifest.getElementsByTagName("Patient"); + if ( patientList.length > 1 ) { + console.warn("More than one patient, loading first one."); + } + // study list + var studyList = patientList[0].getElementsByTagName("Study"); + if ( studyList.length > 1 ) { + console.warn("More than one study, loading first one."); + } + var studyUID = studyList[0].getAttribute("StudyInstanceUID"); + // series list + var seriesList = studyList[0].getElementsByTagName("Series"); + if ( seriesList.length > 1 ) { + console.warn("More than one series, loading first one."); + } + var seriesUID = seriesList[0].getAttribute("SeriesInstanceUID"); + // instance list + var instanceList = seriesList[0].getElementsByTagName("Instance"); + // loop on instances and push links + var max = instanceList.length; + if ( nslices < max ) { + max = nslices; + } + for ( var i = 0; i < max; ++i ) { + var sopInstanceUID = instanceList[i].getAttribute("SOPInstanceUID"); + var link = rootURL + + "&studyUID=" + studyUID + + "&seriesUID=" + seriesUID + + "&objectUID=" + sopInstanceUID; + result.push( link ); + } + // return + return result; +}; + + return dwv; +})); diff --git a/anatomy/static/js/dwv/dwv.min.js b/anatomy/static/js/dwv/dwv.min.js new file mode 100644 index 00000000..67477b10 --- /dev/null +++ b/anatomy/static/js/dwv/dwv.min.js @@ -0,0 +1,3 @@ +/*! dwv 0.23.3 25-03-2018 */ + +!function(e,t){"function"==typeof define&&define.amd?define(["i18next","i18next-xhr-backend","i18next-browser-languagedetector","jszip","konva"],t):"object"==typeof module&&module.exports?module.exports=t(require("i18next"),require("i18next-xhr-backend"),require("i18next-browser-languagedetector"),require("jszip"),null,null):e.dwv=t(e.i18next,e.i18nextXHRBackend,e.i18nextBrowserLanguageDetector,e.JSZip,e.Konva,e.MagicWand)}(this,function(a,s,l,t,T,f){var W=void 0!==W?W:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};(H=H||{}).App=function(){var v,o=this,a=null,s=null,l=null,u=0,c=0,d=0,T=null,r=1,L=!1,S=1,i={x:0,y:0},x={x:0,y:0},g=null,m=null,h=null,I=null,P=null,p=null,f=null,C=new H.html.Style,F=null,A=null,y=null,E=null,D={},w="./";function O(e,t){R([e],new H.io.UrlsLoader,{requestHeaders:t})}function b(e,t,n){y=t;var i=W.onkeydown;W.onkeydown=function(e){e.ctrlKey&&88===e.keyCode&&(console.log("crtl-x pressed!"),o.abortLoad())},o.reset();var r="";r=void 0!==e[0].name?e[0].name:e[0],d=1===e.length&&"zip"!==r.split(".").pop().toLowerCase(),t.setDefaultCharacterSet(v),t.onload=function(e){a&&g.append(e.view),function(e){if(g)return;g=e.view,m=new H.ViewController(g),I&&I.update(e.info);s=g.getImage();var t=(a=s).getGeometry().getSize();u=t.getNumberOfColumns(),c=t.getNumberOfRows(),function(e,t){var n=o.getElement("imageLayer");(p=new H.html.Layer(n)).initialise(e,t),p.fillContext(),p.setStyleDisplay(!0);var i=o.getElement("drawDiv");i&&(f=new H.DrawController(i)).create(e,t),L?o.fitToSize(H.gui.getWindowSize()):o.fitToSize({width:o.getElement("layerContainer").offsetWidth,height:o.getElement("layerContainer").offsetHeight}),o.resetLayout()}(u,c),l=p.getContext().createImageData(u,c),g.addEventListener("wl-width-change",o.onWLChange),g.addEventListener("wl-center-change",o.onWLChange),g.addEventListener("colour-change",o.onColourChange),g.addEventListener("slice-change",o.onSliceChange),g.addEventListener("frame-change",o.onFrameChange),g.addEventListener("wl-width-change",q),g.addEventListener("wl-center-change",q),g.addEventListener("colour-change",q),g.addEventListener("position-change",q),g.addEventListener("slice-change",q),g.addEventListener("frame-change",q);F&&F.initAndDisplay(p);var n=o.getElement("dropBox");if(n){n.removeEventListener("dragover",Q),n.removeEventListener("dragleave",B),n.removeEventListener("drop",V),H.html.removeNode(n);var i=o.getElement("layerContainer");i.addEventListener("dragover",Q),i.addEventListener("dragleave",B),i.addEventListener("drop",V)}o.getElement("infoLayer")&&((h=new H.InfoController(T)).create(o),h.toggleListeners(o,g));o.initWLDisplay(),U()}(e)},t.onerror=function(e){G(e)},t.onabort=function(e){!function(e){e.message?console.warn(e.message):console.warn("Abort called.");H.gui.displayProgress(100)}(e)},t.onloadend=function(){W.onkeydown=i,f&&f.activateDrawLayer(m),q({type:"load-progress",lengthComputable:!0,loaded:100,total:100}),q({type:"load-end"}),y=null},t.onprogress=k,q({type:"load-start"}),t.load(e,n)}function R(e,t,n){t.onload=function(e){var t=new H.State;t.apply(o,t.fromJSON(e))},t.onerror=function(e){G(e)},t.load(e,n)}function q(e){if(void 0!==D[e.type])for(var t=0;tc){for(var p=h.length/c,f=[],C=0,y=0;y";for(var c=1;c]+>/g,"").toLowerCase().indexOf(n[o])<0?i="none":n[o].length&&H.html.highlight(n[o],t.rows[r]),t.rows[r].style.display=i}},H.html.dehighlight=function(e){for(var t=0;t]+>/g,"")),n.parentNode);3!==n.nodeType&&H.html.dehighlight(n)}},H.html.highlight=function(e,t){for(var n=0;n=t){var l=s[t].firstChild.data;s[t].firstChild.data=H.i18n(r+l+o)}}},H.html.makeCellEditable=function(e,t,n){if(void 0!==e){var i=document.createElement("input");t?i.onchange=t:i.disabled=!0,i.value=e.firstChild.data,void 0===n||"color"===n&&!H.browser.hasInputColor()?i.type="text":i.type=n,H.html.cleanNode(e);var r=document.createElement("form");r.onsubmit=function(e){e.preventDefault()},r.appendChild(i),e.appendChild(r)}else console.warn("Cannot create input for non existing cell.")},H.html.setCursorToPointer=function(){document.body.style.cursor="pointer"},H.html.setCursorToDefault=function(){document.body.style.cursor="default"},H.html.createHtmlSelect=function(e,t,n,i){var r=document.createElement("select");r.className=e;var o,a=void 0===n?"":n+".",s=void 0!==i,l=function(e){var t=a+e+".name";return s?H.i18nExists(t)?H.i18n(t):e:H.i18n(t)};if(t instanceof Array)for(var u in t)t.hasOwnProperty(u)&&((o=document.createElement("option")).value=t[u],o.appendChild(document.createTextNode(l(t[u]))),r.appendChild(o));else{if("object"!=typeof t)throw new Error("Unsupported input list type.");for(var c in t)(o=document.createElement("option")).value=c,o.appendChild(document.createTextNode(l(c))),r.appendChild(o)}return r},H.html.displayElement=function(e,t){e.style.display=t?"":"none"},H.html.toggleDisplay=function(e){"none"===e.style.display?e.style.display="":e.style.display="none"},H.html.appendElement=function(e,t){e.appendChild(t),H.gui.refreshElement(e)},H.html.createHiddenElement=function(e,t){var n=document.createElement(e);return n.className=t,n.style.display="none",n},(H=H||{}).gui=H.gui||{},H.gui.base=H.gui.base||{},H.gui.info=H.gui.info||{},H.gui.base.plot=function(){},H.gui.info.MiniColourMap=function(h,p){this.create=function(){var e=h.getElementsByClassName("colour-map-info");0!==e.length&&H.html.removeNodes(e);var t=document.createElement("canvas");t.className="colour-map-info",t.width=98,t.height=10,h.appendChild(t)},this.update=function(e){for(var t,n=e.wc,i=e.ww,r=h.getElementsByClassName("colour-map-info")[0],o=r.getContext("2d"),a=p.getViewController().getColourMap(),s=o.getImageData(0,0,r.width,r.height),l=0,u=p.getImage().getRescaledDataRange().min,c=(p.getImage().getRescaledDataRange().max-u)/r.width,d=0,S=n-.5-(i-1)/2,x=n-.5+(i-1)/2,g=0;gn.getMax()?t:e})},H.image.filter.Sharpen=function(){this.getName=function(){return"Sharpen"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},H.image.filter.Sharpen.prototype.update=function(){return this.getOriginalImage().convolute2D([0,-1,0,-1,5,-1,0,-1,0])},H.image.filter.Sobel=function(){this.getName=function(){return"Sobel"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},H.image.filter.Sobel.prototype.update=function(){var e=this.getOriginalImage(),t=e.convolute2D([1,0,-1,2,0,-2,1,0,-1]),n=e.convolute2D([1,2,1,0,0,0,-1,-2,-1]);return t.compose(n,function(e,t){return Math.sqrt(e*e+t*t)})},(H=H||{}).image=H.image||{},H.image.Size=function(e,t,n){this.getNumberOfColumns=function(){return e},this.getNumberOfRows=function(){return t},this.getNumberOfSlices=function(){return n||1}},H.image.Size.prototype.getSliceSize=function(){return this.getNumberOfColumns()*this.getNumberOfRows()},H.image.Size.prototype.getTotalSize=function(){return this.getSliceSize()*this.getNumberOfSlices()},H.image.Size.prototype.equals=function(e){return null!==e&&this.getNumberOfColumns()===e.getNumberOfColumns()&&this.getNumberOfRows()===e.getNumberOfRows()&&this.getNumberOfSlices()===e.getNumberOfSlices()},H.image.Size.prototype.isInBounds=function(e,t,n){return!(e<0||e>this.getNumberOfColumns()-1||t<0||t>this.getNumberOfRows()-1||n<0||n>this.getNumberOfSlices()-1)},H.image.Size.prototype.toString=function(){return"("+this.getNumberOfColumns()+", "+this.getNumberOfRows()+", "+this.getNumberOfSlices()+")"},H.image.Spacing=function(e,t,n){this.getColumnSpacing=function(){return e},this.getRowSpacing=function(){return t},this.getSliceSpacing=function(){return n||1}},H.image.Spacing.prototype.equals=function(e){return null!==e&&this.getColumnSpacing()===e.getColumnSpacing()&&this.getRowSpacing()===e.getRowSpacing()&&this.getSliceSpacing()===e.getSliceSpacing()},H.image.Spacing.prototype.toString=function(){return"("+this.getColumnSpacing()+", "+this.getRowSpacing()+", "+this.getSliceSpacing()+")"},H.image.Geometry=function(e,n,t,o){void 0===e&&(e=new H.math.Point3D(0,0,0));var a=[e];void 0===o&&(o=new H.math.getIdentityMat33),this.getOrigin=function(){return e},this.getOrigins=function(){return a},this.getSize=function(){return n},this.getSpacing=function(){return t},this.getOrientation=function(){return o},this.getSliceIndex=function(e){for(var t=0,n=e.getDistance(a[0]),i=0,r=0;r=2*H.image.lut.range_max/3?H.image.lut.range_max-1:0},H.image.lut.toMaxFirstThird=function(e){var t=3*e;return t>H.image.lut.range_max-1?H.image.lut.range_max-1:t},H.image.lut.toMaxSecondThird=function(e){var t=H.image.lut.range_max/3,n=0;return t<=e&&(n=3*(e-t))>H.image.lut.range_max-1?H.image.lut.range_max-1:n},H.image.lut.toMaxThirdThird=function(e){var t=H.image.lut.range_max/3,n=0;return 2*t<=e&&(n=3*(e-2*t))>H.image.lut.range_max-1?H.image.lut.range_max-1:n},H.image.lut.zero=function(){return 0},H.image.lut.id=function(e){return e},H.image.lut.invId=function(e){return H.image.lut.range_max-1-e},H.image.lut.plain={red:H.image.lut.buildLut(H.image.lut.id),green:H.image.lut.buildLut(H.image.lut.id),blue:H.image.lut.buildLut(H.image.lut.id)},H.image.lut.invPlain={red:H.image.lut.buildLut(H.image.lut.invId),green:H.image.lut.buildLut(H.image.lut.invId),blue:H.image.lut.buildLut(H.image.lut.invId)},H.image.lut.rainbow={blue:[0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,247,239,231,223,215,207,199,191,183,175,167,159,151,143,135,127,119,111,103,95,87,79,71,63,55,47,39,31,23,15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,251,249,247,245,243,241,239,237,235,233,231,229,227,225,223,221,219,217,215,213,211,209,207,205,203,201,199,197,195,193,192,189,186,183,180,177,174,171,168,165,162,159,156,153,150,147,144,141,138,135,132,129,126,123,120,117,114,111,108,105,102,99,96,93,90,87,84,81,78,75,72,69,66,63,60,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3],red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,62,60,58,56,54,52,50,48,46,44,42,40,38,36,34,32,30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]},H.image.lut.hot={red:H.image.lut.buildLut(H.image.lut.toMaxFirstThird),green:H.image.lut.buildLut(H.image.lut.toMaxSecondThird),blue:H.image.lut.buildLut(H.image.lut.toMaxThirdThird)},H.image.lut.hot_iron={red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255]},H.image.lut.pet={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,128,126,124,122,120,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,76,74,72,70,68,66,64,63,61,59,57,55,53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,252,248,244,240,236,232,228,224,220,216,212,208,204,200,196,192,188,184,180,176,172,168,164,160,156,152,148,144,140,136,132,128,124,120,116,112,108,104,100,96,92,88,84,80,76,72,68,64,60,56,52,48,44,40,36,32,28,24,20,16,12,8,4,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,255]},H.image.lut.hot_metal_blue={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,85,88,90,93,96,99,102,105,108,111,114,116,119,122,125,128,131,134,137,140,143,146,149,152,155,158,161,164,166,169,172,175,178,181,184,187,190,194,198,201,205,209,213,217,221,224,228,232,236,240,244,247,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,9,11,13,15,17,19,21,23,24,26,28,30,32,34,36,38,40,41,43,45,47,49,51,53,55,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,120,122,124,126,128,130,132,134,136,137,139,141,143,145,147,149,151,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,201,203,205,207,209,211,213,215,216,218,220,222,224,226,228,229,231,233,235,237,239,240,242,244,246,248,250,251,253,255],blue:[0,2,4,6,8,10,12,14,16,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,197,194,191,188,185,182,179,176,174,171,168,165,162,159,156,153,150,144,138,132,126,121,115,109,103,97,91,85,79,74,68,62,56,50,47,44,41,38,35,32,29,26,24,21,18,15,12,9,6,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,223,226,229,232,236,239,242,245,249,252,255]},H.image.lut.pet_20step={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,192,192,192,192,176,176,176,176,176,176,176,176,176,176,176,176,176,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,96,96,96,96,96,96,96,96,96,96,96,96,96,144,144,144,144,144,144,144,144,144,144,144,144,144,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,176,176,176,176,176,176,176,176,176,176,176,176,176,144,144,144,144,144,144,144,144,144,144,144,144,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,128,176,176,176,176,176,176,176,176,176,176,176,176,176,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,64,64,64,64,64,64,64,64,64,64,64,64,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255]},H.image.lut.test={red:H.image.lut.buildLut(H.image.lut.id),green:H.image.lut.buildLut(H.image.lut.zero),blue:H.image.lut.buildLut(H.image.lut.zero)},(H=H||{}).image=H.image||{},H.image.WindowLevel=function(t,n){if(0===n)throw new Error("A window level with a width of zero is not possible.");var i=0,r=0,o=255,a=null,s=null,l=null,u=null;function c(){var e=t+i;a=e-.5-(n-1)/2,s=e-.5+(n-1)/2,l=(o-r)/(n-1),u=(-(e-.5)/(n-1)+.5)*(o-r)+r}c(),this.getCenter=function(){return t},this.getWidth=function(){return n},this.setRange=function(e,t){r=parseInt(e,10),o=parseInt(t,10),c()},this.setSignedOffset=function(e){i=e,c()},this.apply=function(e){return e<=a?r:s=d.getNumberOfFrames())&&(n!==(n=e)&&1!==d.getNumberOfFrames()&&(this.fireEvent({type:"frame-change",frame:n}),this.setCurrentPosition(this.getCurrentPosition(),!0)),!0)},this.append=function(e){var t=this.getImage().appendSlice(e.getImage());t<=this.getCurrentPosition().k&&this.setCurrentPosition({i:this.getCurrentPosition().i,j:this.getCurrentPosition().j,k:this.getCurrentPosition().k+1},!0),this.addWindowPresets(e.getWindowPresets(),t)},this.appendFrameBuffer=function(e){this.getImage().appendFrameBuffer(e)},this.setWindowLevel=function(e,t,n){if(1<=t){var i=this.getCurrentPosition().k,r=null,o=d.getRescaleSlopeAndIntercept(i);if(o&&void 0!==o){var a=S[o.toString()];a&&void 0!==a&&(r=a.getWindowLevel())}void 0===n&&(n="manual"),x=n;var s=new H.image.WindowLevel(e,t);if(0===Object.keys(S).length){var l=new H.image.lut.Rescale(d.getRescaleSlopeAndIntercept(0),d.getMeta().BitsStored),u=new H.image.lut.Window(l,d.getMeta().IsSigned);this.addWindowLut(u)}for(var c in S)S[c].setWindowLevel(s);r&&void 0!==r?(r.getWidth()!==t&&this.fireEvent({type:"wl-width-change",wc:e,ww:t}),r.getCenter()!==e&&this.fireEvent({type:"wl-center-change",wc:e,ww:t})):(this.fireEvent({type:"wl-width-change",wc:e,ww:t}),this.fireEvent({type:"wl-center-change",wc:e,ww:t}))}},this.setWindowLevelPreset=function(e){var t=this.getWindowPresets()[e];if(void 0===t)throw new Error("Unknown window level preset: '"+e+"'");"minmax"===e&&void 0===t.wl&&(t.wl=this.getWindowLevelMinMax()),void 0!==t.perslice&&!0===t.perslice&&(t={wl:t.wl[this.getCurrentPosition().k]}),this.setWindowLevel(t.wl.getCenter(),t.wl.getWidth(),e)},this.setWindowLevelPresetById=function(e){var t=Object.keys(this.getWindowPresets());this.setWindowLevelPreset(t[e])},this.clone=function(){var e=new H.image.View(this.getImage());for(var t in S)e.addWindowLut(S[t]);return e.setListeners(this.getListeners()),e};var r={};this.getListeners=function(){return r},this.setListeners=function(e){r=e}},H.image.View.prototype.getWindowLevelMinMax=function(){var e=this.getImage().getRescaledDataRange(),t=e.min,n=e.max-t,i=t+n/2;return new H.image.WindowLevel(i,n)},H.image.View.prototype.setWindowLevelMinMax=function(){var e=this.getWindowLevelMinMax();this.setWindowLevel(e.getCenter(),e.getWidth(),"minmax")},H.image.View.prototype.generateImageData=function(e){var t=this.getCurrentWindowLut(),n=this.getImage(),i=n.getGeometry().getSize().getSliceSize(),r=i*this.getCurrentPosition().k,o=this.getCurrentFrame()?this.getCurrentFrame():0,a=0,s=0,l=0,u=n.getPhotometricInterpretation();switch(u){case"MONOCHROME1":case"MONOCHROME2":for(var c=this.getColourMap(),d=r+i,S=r;Sn.getEnd().getX()?0:-1,x=n.getBegin().getY()>n.getEnd().getY()?-1:.5,g=new T.Label({x:n.getEnd().getX()+25*S,y:n.getEnd().getY()+15*x,name:"label"});g.add(d),g.add(new T.Tag);var m=new T.Group;return m.name("line-group"),m.add(i),m.add(c),m.add(g),m.visible(!0),m},H.tool.UpdateArrow=function(e){var t=e.getParent(),n=t.getChildren(function(e){return"shape"===e.name()})[0],i=t.getChildren(function(e){return"shape-triangle"===e.name()})[0],r=t.getChildren(function(e){return"label"===e.name()})[0],o=t.getChildren(function(e){return"begin"===e.id()})[0],a=t.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":o.x(e.x()),o.y(e.y());break;case"end":a.x(e.x()),a.y(e.y())}var s=o.x()-n.x(),l=o.y()-n.y(),u=a.x()-n.x(),c=a.y()-n.y();n.points([s,l,u,c]);var d=new H.math.Point2D(o.x(),o.y()),S=new H.math.Point2D(a.x(),a.y()),x=new H.math.Line(d,S),g=new H.math.Point2D(s,l),m=new H.math.Point2D(u,c),h=H.math.getPerpendicularLine(x,g,10),p=H.math.getPerpendicularLine(x,m,10);n.hitFunc(function(e){e.beginPath(),e.moveTo(h.getBegin().getX(),h.getBegin().getY()),e.lineTo(h.getEnd().getX(),h.getEnd().getY()),e.lineTo(p.getEnd().getX(),p.getEnd().getY()),e.lineTo(p.getBegin().getX(),p.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var f=new H.math.Point2D(x.getBegin().getX(),x.getBegin().getY()-10),C=new H.math.Line(x.getBegin(),f),y=H.math.getAngle(x,C),D=y*Math.PI/180;i.x(x.getBegin().getX()+i.radius()*Math.sin(D)),i.y(x.getBegin().getY()+i.radius()*Math.cos(D)),i.rotation(-y);var v=r.getText();v.quant=null,v.setText(v.textExpr);var T=x.getBegin().getX()>x.getEnd().getX()?0:-1,L=x.getBegin().getY()>x.getEnd().getY()?-1:.5,I={x:x.getEnd().getX()+25*T,y:x.getEnd().getY()+15*L};r.position(I)},(H=H||{}).tool=H.tool||{};T=T||{};H.tool.Draw=function(d,e){var n=this,i=null,r=!1;this.shapeFactoryList=e;var t=null,o=null;this.shapeName=0;var a=[],s=null,S=new H.tool.ShapeEditor(d);S.setDrawEventCallback(f);var x=new T.Group,l=new T.Line({points:[-10,-10,10,10],stroke:"red"}),u=new T.Line({points:[10,-10,-10,10],stroke:"red"});x.add(l),x.add(u),this.style=new H.html.Style;var c={},g=null;function m(){h(!0)}function h(e){g.listening(e),g.hitGraphEnabled(e);var t=d.getDrawController().getCurrentPosGroup().getChildren();e?(d.addToolCanvasListeners(d.getDrawStage().getContent()),t.forEach(function(e){n.setShapeOn(e)})):(d.removeToolCanvasListeners(d.getDrawStage().getContent()),t.forEach(function(e){var t;(t=e).off("mouseover"),t.off("mouseout"),t.draggable(!1),t.off("dragstart.draw"),t.off("dragmove.draw"),t.off("dragend.draw"),t.off("dblclick")})),g.draw()}function p(e){var t=d.getDrawStage();return{x:t.offset().x+e.x/t.scale().x,y:t.offset().y+e.y/t.scale().y}}function f(e){if(void 0!==c[e.type])for(var t=0;ti.getEnd().getX()?0:-1,S=i.getBegin().getY()>i.getEnd().getY()?-1:.5,x=new T.Label({x:i.getEnd().getX()+25*d,y:i.getEnd().getY()+15*S,name:"label"});x.add(c),x.add(new T.Tag);var g=new T.Group;return g.name("ruler-group"),g.add(r),g.add(a),g.add(l),g.add(x),g.visible(!0),g},H.tool.UpdateRuler=function(e,t){var n=e.getParent(),i=n.getChildren(function(e){return"shape"===e.name()})[0],r=n.getChildren(function(e){return"shape-tick0"===e.name()})[0],o=n.getChildren(function(e){return"shape-tick1"===e.name()})[0],a=n.getChildren(function(e){return"label"===e.name()})[0],s=n.getChildren(function(e){return"begin"===e.id()})[0],l=n.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":s.x(e.x()),s.y(e.y());break;case"end":l.x(e.x()),l.y(e.y())}var u=s.x()-i.x(),c=s.y()-i.y(),d=l.x()-i.x(),S=l.y()-i.y();i.points([u,c,d,S]);var x=new H.math.Point2D(s.x(),s.y()),g=new H.math.Point2D(l.x(),l.y()),m=new H.math.Line(x,g),h=new H.math.Point2D(u,c),p=new H.math.Point2D(d,S),f=H.math.getPerpendicularLine(m,h,10);r.points([f.getBegin().getX(),f.getBegin().getY(),f.getEnd().getX(),f.getEnd().getY()]);var C=H.math.getPerpendicularLine(m,p,10);o.points([C.getBegin().getX(),C.getBegin().getY(),C.getEnd().getX(),C.getEnd().getY()]),i.hitFunc(function(e){e.beginPath(),e.moveTo(f.getBegin().getX(),f.getBegin().getY()),e.lineTo(f.getEnd().getX(),f.getEnd().getY()),e.lineTo(C.getEnd().getX(),C.getEnd().getY()),e.lineTo(C.getBegin().getX(),C.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});var y=t.quantifyLine(m),D=a.getText();D.quant=y,D.setText(H.utils.replaceFlags(D.textExpr,D.quant));var v=m.getBegin().getX()>m.getEnd().getX()?0:-1,T=m.getBegin().getY()>m.getEnd().getY()?-1:.5,L={x:m.getEnd().getX()+25*v,y:m.getEnd().getY()+15*T};a.position(L)},(H=H||{}).tool=H.tool||{},H.tool.Scroll=function(o){var a=this,t=null;this.started=!1;var n=null;function i(e){var t=1!==o.getImage().getGeometry().getSize().getNumberOfSlices(),n=1!==o.getImage().getNumberOfFrames();e?t?o.getViewController().incrementSliceNb():n&&o.getViewController().incrementFrameNb():t?o.getViewController().decrementSliceNb():n&&o.getViewController().decrementFrameNb()}this.mousedown=function(e){o.getViewController().isPlaying()&&o.getViewController().stop(),a.started=!0,a.x0=e._x,a.y0=e._y},this.mousemove=function(e){if(a.started){var t=e._y-a.y0,n=15-1?t.replace(/###/g,"."):t}function r(){return!t||"string"==typeof t}for(var i="string"!=typeof e?[].concat(e):e.split(".");i.length>1;){if(r())return{};var s=o(i.shift());!t[s]&&n&&(t[s]=new n),t=t[s]}return r()?{}:{obj:t,k:o(i.shift())}}function o(t,e,o){var r=n(t,e,Object),i=r.obj,s=r.k;i[s]=o}function r(t,e,o,r){var i=n(t,e,Object),s=i.obj,a=i.k;s[a]=s[a]||[],r&&(s[a]=s[a].concat(o)),r||s[a].push(o)}function i(t,e){var o=n(t,e),r=o.obj,i=o.k;if(r)return r[i]}function s(t,e,n){for(var o in e)o in t?"string"==typeof t[o]||t[o]instanceof String||"string"==typeof e[o]||e[o]instanceof String?n&&(t[o]=e[o]):s(t[o],e[o],n):t[o]=e[o];return t}function a(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}function u(t){return"string"==typeof t?t.replace(/[&<>"'\/]/g,function(t){return L[t]}):t}function l(t){return t.charAt(0).toUpperCase()+t.slice(1)}function p(){var t={};return P.forEach(function(e){e.lngs.forEach(function(n){t[n]={numbers:e.nr,plurals:F[e.fc]}})}),t}function c(t,e){for(var n=t.indexOf(e);n!==-1;)t.splice(n,1),n=t.indexOf(e)}function f(){return{debug:!1,initImmediate:!0,ns:["translation"],defaultNS:["translation"],fallbackLng:["dev"],fallbackNS:!1,whitelist:!1,nonExplicitWhitelist:!1,load:"all",preload:!1,simplifyPluralSuffix:!0,keySeparator:".",nsSeparator:":",pluralSeparator:"_",contextSeparator:"_",saveMissing:!1,updateMissing:!1,saveMissingTo:"fallback",saveMissingPlurals:!0,missingKeyHandler:!1,missingInterpolationHandler:!1,postProcess:!1,returnNull:!0,returnEmptyString:!0,returnObjects:!1,joinArrays:!1,returnedObjectHandler:function(){},parseMissingKeyHandler:!1,appendNamespaceToMissingKey:!1,appendNamespaceToCIMode:!1,overloadTranslationOptionHandler:function(t){var e={};return t[1]&&(e.defaultValue=t[1]),t[2]&&(e.tDescription=t[2]),e},interpolation:{escapeValue:!0,format:function(t,e,n){return t},prefix:"{{",suffix:"}}",formatSeparator:",",unescapePrefix:"-",nestingPrefix:"$t(",nestingSuffix:")",maxReplaces:1e3}}}function h(t){return"string"==typeof t.ns&&(t.ns=[t.ns]),"string"==typeof t.fallbackLng&&(t.fallbackLng=[t.fallbackLng]),"string"==typeof t.fallbackNS&&(t.fallbackNS=[t.fallbackNS]),t.whitelist&&t.whitelist.indexOf("cimode")<0&&(t.whitelist=t.whitelist.concat(["cimode"])),t}function g(){}var d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},y=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},v=Object.assign||function(t){for(var e=1;e1&&void 0!==arguments[1]?arguments[1]:{};y(this,t),this.init(e,n)}return t.prototype.init=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.prefix=e.prefix||"i18next:",this.logger=t||S,this.options=e,this.debug=e.debug},t.prototype.setDebug=function(t){this.debug=t},t.prototype.log=function(){for(var t=arguments.length,e=Array(t),n=0;n-1&&n.observers[t].splice(o,1)}else delete n.observers[t]})},t.prototype.emit=function(t){for(var e=arguments.length,n=Array(e>1?e-1:0),o=1;o":">",'"':""","'":"'","/":"/"},N=function(t){function e(n){var o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{ns:["translation"],defaultNS:"translation"};y(this,e);var r=b(this,t.call(this));return r.data=n||{},r.options=o,void 0===r.options.keySeparator&&(r.options.keySeparator="."),r}return m(e,t),e.prototype.addNamespaces=function(t){this.options.ns.indexOf(t)<0&&this.options.ns.push(t)},e.prototype.removeNamespaces=function(t){var e=this.options.ns.indexOf(t);e>-1&&this.options.ns.splice(e,1)},e.prototype.getResource=function(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},r=void 0!==o.keySeparator?o.keySeparator:this.options.keySeparator,s=[t,e];return n&&"string"!=typeof n&&(s=s.concat(n)),n&&"string"==typeof n&&(s=s.concat(r?n.split(r):n)),t.indexOf(".")>-1&&(s=t.split(".")),i(this.data,s)},e.prototype.addResource=function(t,e,n,r){var i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{silent:!1},s=this.options.keySeparator;void 0===s&&(s=".");var a=[t,e];n&&(a=a.concat(s?n.split(s):n)),t.indexOf(".")>-1&&(a=t.split("."),r=e,e=a[1]),this.addNamespaces(e),o(this.data,a,r),i.silent||this.emit("added",t,e,n,r)},e.prototype.addResources=function(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{silent:!1};for(var r in n)"string"==typeof n[r]&&this.addResource(t,e,r,n[r],{silent:!0});o.silent||this.emit("added",t,e,n)},e.prototype.addResourceBundle=function(t,e,n,r,a){var u=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{silent:!1},l=[t,e];t.indexOf(".")>-1&&(l=t.split("."),r=n,n=e,e=l[1]),this.addNamespaces(e);var p=i(this.data,l)||{};r?s(p,n,a):p=v({},p,n),o(this.data,l,p),u.silent||this.emit("added",t,e,n)},e.prototype.removeResourceBundle=function(t,e){this.hasResourceBundle(t,e)&&delete this.data[t][e],this.removeNamespaces(e),this.emit("removed",t,e)},e.prototype.hasResourceBundle=function(t,e){return void 0!==this.getResource(t,e)},e.prototype.getResourceBundle=function(t,e){return e||(e=this.options.defaultNS),"v1"===this.options.compatibilityAPI?v({},this.getResource(t,e)):this.getResource(t,e)},e.prototype.toJSON=function(){return this.data},e}(R),C={processors:{},addPostProcessor:function(t){this.processors[t.name]=t},handle:function(t,e,n,o,r){var i=this;return t.forEach(function(t){i.processors[t]&&(e=i.processors[t].process(e,n,o,r))}),e}},j=function(t){function n(o){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};y(this,n);var i=b(this,t.call(this));return e(["resourceStore","languageUtils","pluralResolver","interpolator","backendConnector","i18nFormat"],o,i),i.options=r,void 0===i.options.keySeparator&&(i.options.keySeparator="."),i.logger=O.create("translator"),i}return m(n,t),n.prototype.changeLanguage=function(t){t&&(this.language=t)},n.prototype.exists=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{interpolation:{}},n=this.resolve(t,e);return n&&void 0!==n.res},n.prototype.extractFromKey=function(t,e){var n=e.nsSeparator||this.options.nsSeparator;void 0===n&&(n=":");var o=void 0!==e.keySeparator?e.keySeparator:this.options.keySeparator,r=e.ns||this.options.defaultNS;if(n&&t.indexOf(n)>-1){var i=t.split(n);(n!==o||n===o&&this.options.ns.indexOf(i[0])>-1)&&(r=i.shift()),t=i.join(o)}return"string"==typeof r&&(r=[r]),{key:t,namespaces:r}},n.prototype.translate=function(t,e){var n=this;if("object"!==("undefined"==typeof e?"undefined":d(e))&&this.options.overloadTranslationOptionHandler&&(e=this.options.overloadTranslationOptionHandler(arguments)),e||(e={}),void 0===t||null===t||""===t)return"";"number"==typeof t&&(t=String(t)),"string"==typeof t&&(t=[t]);var o=void 0!==e.keySeparator?e.keySeparator:this.options.keySeparator,r=this.extractFromKey(t[t.length-1],e),i=r.key,s=r.namespaces,a=s[s.length-1],u=e.lng||this.language,l=e.appendNamespaceToCIMode||this.options.appendNamespaceToCIMode;if(u&&"cimode"===u.toLowerCase()){if(l){var p=e.nsSeparator||this.options.nsSeparator;return a+p+i}return i}var c=this.resolve(t,e),f=c&&c.res,h=c&&c.usedKey||i,g=Object.prototype.toString.apply(f),y=["[object Number]","[object Function]","[object RegExp]"],m=void 0!==e.joinArrays?e.joinArrays:this.options.joinArrays,b="string"!=typeof f&&"boolean"!=typeof f&&"number"!=typeof f;if(f&&b&&y.indexOf(g)<0&&(!m||"[object Array]"!==g)){if(!e.returnObjects&&!this.options.returnObjects)return this.logger.warn("accessing an object - but returnObjects options is not enabled!"),this.options.returnedObjectHandler?this.options.returnedObjectHandler(h,f,e):"key '"+i+" ("+this.language+")' returned an object instead of string.";if(o){var x="[object Array]"===g?[]:{};for(var k in f)if(Object.prototype.hasOwnProperty.call(f,k)){var S=""+h+o+k;x[k]=this.translate(S,v({},e,{joinArrays:!1,ns:s})),x[k]===S&&(x[k]=f[k])}f=x}}else if(m&&"[object Array]"===g)f=f.join(m),f&&(f=this.extendTranslation(f,t,e));else{var w=!1,O=!1;this.isValidLookup(f)||void 0===e.defaultValue||(w=!0,f=e.defaultValue),this.isValidLookup(f)||(O=!0,f=i);var R=e.defaultValue&&e.defaultValue!==f&&this.options.updateMissing;if(O||w||R){this.logger.log(R?"updateKey":"missingKey",u,a,i,R?e.defaultValue:f);var L=[],N=this.languageUtils.getFallbackCodes(this.options.fallbackLng,e.lng||this.language);if("fallback"===this.options.saveMissingTo&&N&&N[0])for(var C=0;C1&&void 0!==arguments[1]?arguments[1]:{},o=void 0,r=void 0,i=void 0,s=void 0;return"string"==typeof t&&(t=[t]),t.forEach(function(t){if(!e.isValidLookup(o)){var a=e.extractFromKey(t,n),u=a.key;r=u;var l=a.namespaces;e.options.fallbackNS&&(l=l.concat(e.options.fallbackNS));var p=void 0!==n.count&&"string"!=typeof n.count,c=void 0!==n.context&&"string"==typeof n.context&&""!==n.context,f=n.lngs?n.lngs:e.languageUtils.toResolveHierarchy(n.lng||e.language);l.forEach(function(t){e.isValidLookup(o)||(s=t,f.forEach(function(r){if(!e.isValidLookup(o)){i=r;var s=u,a=[s];if(e.i18nFormat&&e.i18nFormat.addLookupKeys)e.i18nFormat.addLookupKeys(a,u,r,t,n);else{var l=void 0;p&&(l=e.pluralResolver.getSuffix(r,n.count)),p&&c&&a.push(s+l),c&&a.push(s+=""+e.options.contextSeparator+n.context),p&&a.push(s+=l)}for(var f=void 0;f=a.pop();)e.isValidLookup(o)||(o=e.getResource(r,t,f,n))}}))})}}),{res:o,usedKey:r,usedLng:i,usedNS:s}},n.prototype.isValidLookup=function(t){return!(void 0===t||!this.options.returnNull&&null===t||!this.options.returnEmptyString&&""===t)},n.prototype.getResource=function(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};return this.resourceStore.getResource(t,e,n,o)},n}(R),E=function(){function t(e){y(this,t),this.options=e,this.whitelist=this.options.whitelist||!1,this.logger=O.create("languageUtils")}return t.prototype.getScriptPartFromCode=function(t){if(!t||t.indexOf("-")<0)return null;var e=t.split("-");return 2===e.length?null:(e.pop(),this.formatLanguageCode(e.join("-")))},t.prototype.getLanguagePartFromCode=function(t){if(!t||t.indexOf("-")<0)return t;var e=t.split("-");return this.formatLanguageCode(e[0])},t.prototype.formatLanguageCode=function(t){if("string"==typeof t&&t.indexOf("-")>-1){var e=["hans","hant","latn","cyrl","cans","mong","arab"],n=t.split("-");return this.options.lowerCaseLng?n=n.map(function(t){return t.toLowerCase()}):2===n.length?(n[0]=n[0].toLowerCase(),n[1]=n[1].toUpperCase(),e.indexOf(n[1].toLowerCase())>-1&&(n[1]=l(n[1].toLowerCase()))):3===n.length&&(n[0]=n[0].toLowerCase(),2===n[1].length&&(n[1]=n[1].toUpperCase()),"sgn"!==n[0]&&2===n[2].length&&(n[2]=n[2].toUpperCase()),e.indexOf(n[1].toLowerCase())>-1&&(n[1]=l(n[1].toLowerCase())),e.indexOf(n[2].toLowerCase())>-1&&(n[2]=l(n[2].toLowerCase()))),n.join("-")}return this.options.cleanCode||this.options.lowerCaseLng?t.toLowerCase():t},t.prototype.isWhitelisted=function(t){return("languageOnly"===this.options.load||this.options.nonExplicitWhitelist)&&(t=this.getLanguagePartFromCode(t)),!this.whitelist||!this.whitelist.length||this.whitelist.indexOf(t)>-1},t.prototype.getFallbackCodes=function(t,e){if(!t)return[];if("string"==typeof t&&(t=[t]),"[object Array]"===Object.prototype.toString.apply(t))return t;if(!e)return t.default||[];var n=t[e];return n||(n=t[this.getScriptPartFromCode(e)]),n||(n=t[this.formatLanguageCode(e)]),n||(n=t.default),n||[]},t.prototype.toResolveHierarchy=function(t,e){var n=this,o=this.getFallbackCodes(e||this.options.fallbackLng||[],t),r=[],i=function(t){t&&(n.isWhitelisted(t)?r.push(t):n.logger.warn("rejecting non-whitelisted language code: "+t))};return"string"==typeof t&&t.indexOf("-")>-1?("languageOnly"!==this.options.load&&i(this.formatLanguageCode(t)),"languageOnly"!==this.options.load&&"currentOnly"!==this.options.load&&i(this.getScriptPartFromCode(t)),"currentOnly"!==this.options.load&&i(this.getLanguagePartFromCode(t))):"string"==typeof t&&i(this.formatLanguageCode(t)),o.forEach(function(t){r.indexOf(t)<0&&i(n.formatLanguageCode(t))}),r},t}(),P=[{lngs:["ach","ak","am","arn","br","fil","gun","ln","mfe","mg","mi","oc","pt","pt-BR","tg","ti","tr","uz","wa"],nr:[1,2],fc:1},{lngs:["af","an","ast","az","bg","bn","ca","da","de","dev","el","en","eo","es","et","eu","fi","fo","fur","fy","gl","gu","ha","he","hi","hu","hy","ia","it","kn","ku","lb","mai","ml","mn","mr","nah","nap","nb","ne","nl","nn","no","nso","pa","pap","pms","ps","pt-PT","rm","sco","se","si","so","son","sq","sv","sw","ta","te","tk","ur","yo"],nr:[1,2],fc:2},{lngs:["ay","bo","cgg","fa","id","ja","jbo","ka","kk","km","ko","ky","lo","ms","sah","su","th","tt","ug","vi","wo","zh"],nr:[1],fc:3},{lngs:["be","bs","dz","hr","ru","sr","uk"],nr:[1,2,5],fc:4},{lngs:["ar"],nr:[0,1,2,3,11,100],fc:5},{lngs:["cs","sk"],nr:[1,2,5],fc:6},{lngs:["csb","pl"],nr:[1,2,5],fc:7},{lngs:["cy"],nr:[1,2,3,8],fc:8},{lngs:["fr"],nr:[1,2],fc:9},{lngs:["ga"],nr:[1,2,3,7,11],fc:10},{lngs:["gd"],nr:[1,2,3,20],fc:11},{lngs:["is"],nr:[1,2],fc:12},{lngs:["jv"],nr:[0,1],fc:13},{lngs:["kw"],nr:[1,2,3,4],fc:14},{lngs:["lt"],nr:[1,2,10],fc:15},{lngs:["lv"],nr:[1,2,0],fc:16},{lngs:["mk"],nr:[1,2],fc:17},{lngs:["mnk"],nr:[0,1,2],fc:18},{lngs:["mt"],nr:[1,2,11,20],fc:19},{lngs:["or"],nr:[2,1],fc:2},{lngs:["ro"],nr:[1,2,20],fc:20},{lngs:["sl"],nr:[5,1,2,3],fc:21}],F={1:function(t){return Number(t>1)},2:function(t){return Number(1!=t)},3:function(t){return 0},4:function(t){return Number(t%10==1&&t%100!=11?0:t%10>=2&&t%10<=4&&(t%100<10||t%100>=20)?1:2)},5:function(t){return Number(0===t?0:1==t?1:2==t?2:t%100>=3&&t%100<=10?3:t%100>=11?4:5)},6:function(t){return Number(1==t?0:t>=2&&t<=4?1:2)},7:function(t){return Number(1==t?0:t%10>=2&&t%10<=4&&(t%100<10||t%100>=20)?1:2)},8:function(t){return Number(1==t?0:2==t?1:8!=t&&11!=t?2:3)},9:function(t){return Number(t>=2)},10:function(t){return Number(1==t?0:2==t?1:t<7?2:t<11?3:4)},11:function(t){return Number(1==t||11==t?0:2==t||12==t?1:t>2&&t<20?2:3)},12:function(t){return Number(t%10!=1||t%100==11)},13:function(t){return Number(0!==t)},14:function(t){return Number(1==t?0:2==t?1:3==t?2:3)},15:function(t){return Number(t%10==1&&t%100!=11?0:t%10>=2&&(t%100<10||t%100>=20)?1:2)},16:function(t){return Number(t%10==1&&t%100!=11?0:0!==t?1:2)},17:function(t){return Number(1==t||t%10==1?0:1)},18:function(t){return Number(0==t?0:1==t?1:2)},19:function(t){return Number(1==t?0:0===t||t%100>1&&t%100<11?1:t%100>10&&t%100<20?2:3)},20:function(t){return Number(1==t?0:0===t||t%100>0&&t%100<20?1:2)},21:function(t){return Number(t%100==1?1:t%100==2?2:t%100==3||t%100==4?3:0)}},A=function(){function t(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};y(this,t),this.languageUtils=e,this.options=n,this.logger=O.create("pluralResolver"),this.rules=p()}return t.prototype.addRule=function(t,e){this.rules[t]=e},t.prototype.getRule=function(t){return this.rules[t]||this.rules[this.languageUtils.getLanguagePartFromCode(t)]},t.prototype.needsPlural=function(t){var e=this.getRule(t);return e&&e.numbers.length>1},t.prototype.getPluralFormsOfKey=function(t,e){var n=this,o=[],r=this.getRule(t);return r?(r.numbers.forEach(function(r){var i=n.getSuffix(t,r);o.push(""+e+i)}),o):o},t.prototype.getSuffix=function(t,e){var n=this,o=this.getRule(t);if(o){var r=o.noAbs?o.plurals(e):o.plurals(Math.abs(e)),i=o.numbers[r];this.options.simplifyPluralSuffix&&2===o.numbers.length&&1===o.numbers[0]&&(2===i?i="plural":1===i&&(i=""));var s=function(){return n.options.prepend&&i.toString()?n.options.prepend+i.toString():i.toString()};return"v1"===this.options.compatibilityJSON?1===i?"":"number"==typeof i?"_plural_"+i.toString():s():"v2"===this.options.compatibilityJSON||2===o.numbers.length&&1===o.numbers[0]?s():2===o.numbers.length&&1===o.numbers[0]?s():this.options.prepend&&r.toString()?this.options.prepend+r.toString():r.toString()}return this.logger.warn("no plural rule found for: "+t),""},t}(),U=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};y(this,e),this.logger=O.create("interpolator"),this.init(t,!0)}return e.prototype.init=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},e=arguments[1];e&&(this.options=t,this.format=t.interpolation&&t.interpolation.format||function(t){return t},this.escape=t.interpolation&&t.interpolation.escape||u),t.interpolation||(t.interpolation={escapeValue:!0});var n=t.interpolation;this.escapeValue=void 0===n.escapeValue||n.escapeValue,this.prefix=n.prefix?a(n.prefix):n.prefixEscaped||"{{",this.suffix=n.suffix?a(n.suffix):n.suffixEscaped||"}}",this.formatSeparator=n.formatSeparator?n.formatSeparator:n.formatSeparator||",",this.unescapePrefix=n.unescapeSuffix?"":n.unescapePrefix||"-",this.unescapeSuffix=this.unescapePrefix?"":n.unescapeSuffix||"",this.nestingPrefix=n.nestingPrefix?a(n.nestingPrefix):n.nestingPrefixEscaped||a("$t("),this.nestingSuffix=n.nestingSuffix?a(n.nestingSuffix):n.nestingSuffixEscaped||a(")"),this.maxReplaces=n.maxReplaces?n.maxReplaces:1e3,this.resetRegExp()},e.prototype.reset=function(){this.options&&this.init(this.options)},e.prototype.resetRegExp=function(){var t=this.prefix+"(.+?)"+this.suffix;this.regexp=new RegExp(t,"g");var e=""+this.prefix+this.unescapePrefix+"(.+?)"+this.unescapeSuffix+this.suffix;this.regexpUnescape=new RegExp(e,"g");var n=this.nestingPrefix+"(.+?)"+this.nestingSuffix;this.nestingRegexp=new RegExp(n,"g")},e.prototype.interpolate=function(e,n,o){function r(t){return t.replace(/\$/g,"$$$$")}var s=this,a=void 0,u=void 0,l=void 0,p=function(t){if(t.indexOf(s.formatSeparator)<0)return i(n,t);var e=t.split(s.formatSeparator),r=e.shift().trim(),a=e.join(s.formatSeparator).trim();return s.format(i(n,r),a,o)};for(this.resetRegExp(),l=0;(a=this.regexpUnescape.exec(e))&&(u=p(a[1].trim()),e=e.replace(a[0],u),this.regexpUnescape.lastIndex=0,l++,!(l>=this.maxReplaces)););for(l=0;a=this.regexp.exec(e);){if(u=p(a[1].trim()),void 0===u)if("function"==typeof this.options.missingInterpolationHandler){var c=this.options.missingInterpolationHandler(e,a);u="string"==typeof c?c:""}else this.logger.warn("missed to pass in variable "+a[1]+" for interpolating "+e),u="";else"string"!=typeof u&&(u=t(u));if(u=r(this.escapeValue?this.escape(u):u),e=e.replace(a[0],u),this.regexp.lastIndex=0,l++,l>=this.maxReplaces)break}return e},e.prototype.nest=function(e,n){function o(t,e){if(t.indexOf(",")<0)return t;var n=t.split(",");t=n.shift();var o=n.join(",");o=this.interpolate(o,a),o=o.replace(/'/g,'"');try{a=JSON.parse(o),e&&(a=v({},e,a))}catch(e){this.logger.error("failed parsing options string in nesting for key "+t,e)}return t}var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=void 0,s=void 0,a=v({},r);for(a.applyPostProcessor=!1;i=this.nestingRegexp.exec(e);){if(s=n(o.call(this,i[1].trim(),a),a),s&&i[0]===e&&"string"!=typeof s)return s;"string"!=typeof s&&(s=t(s)),s||(this.logger.warn("missed to resolve "+i[1]+" for nesting "+e),s=""),e=e.replace(i[0],s),this.regexp.lastIndex=0}return e},e}(),T=function(t){function e(n,o,r){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};y(this,e);var s=b(this,t.call(this));return s.backend=n,s.store=o,s.languageUtils=r.languageUtils,s.options=i,s.logger=O.create("backendConnector"),s.state={},s.queue=[],s.backend&&s.backend.init&&s.backend.init(r,i.backend,i),s}return m(e,t),e.prototype.queueLoad=function(t,e,n){var o=this,r=[],i=[],s=[],a=[];return t.forEach(function(t){var n=!0;e.forEach(function(e){var s=t+"|"+e;o.store.hasResourceBundle(t,e)?o.state[s]=2:o.state[s]<0||(1===o.state[s]?i.indexOf(s)<0&&i.push(s):(o.state[s]=1,n=!1,i.indexOf(s)<0&&i.push(s),r.indexOf(s)<0&&r.push(s),a.indexOf(e)<0&&a.push(e)))}),n||s.push(t)}),(r.length||i.length)&&this.queue.push({pending:i,loaded:{},errors:[],callback:n}),{toLoad:r,pending:i,toLoadLanguages:s,toLoadNamespaces:a}},e.prototype.loaded=function(t,e,n){var o=this,i=t.split("|"),s=x(i,2),a=s[0],u=s[1];e&&this.emit("failedLoading",a,u,e),n&&this.store.addResourceBundle(a,u,n),this.state[t]=e?-1:2,this.queue.forEach(function(n){r(n.loaded,[a],u),c(n.pending,t),e&&n.errors.push(e),0!==n.pending.length||n.done||(o.emit("loaded",n.loaded),n.done=!0,n.errors.length?n.callback(n.errors):n.callback())}),this.queue=this.queue.filter(function(t){return!t.done})},e.prototype.read=function(t,e,n){var o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=this,i=arguments.length>4&&void 0!==arguments[4]?arguments[4]:250,s=arguments[5];return t.length?this.backend[n](t,e,function(a,u){return a&&u&&o<5?void setTimeout(function(){r.read.call(r,t,e,n,o+1,2*i,s)},i):void s(a,u)}):s(null,{})},e.prototype.load=function(t,e,n){var o=this;if(!this.backend)return this.logger.warn("No backend was added via i18next.use. Will not load resources."),n&&n();"string"==typeof t&&(t=this.languageUtils.toResolveHierarchy(t)),"string"==typeof e&&(e=[e]);var r=this.queueLoad(t,e,n);return r.toLoad.length?void r.toLoad.forEach(function(t){o.loadOne(t)}):(r.pending.length||n(),null)},e.prototype.reload=function(t,e){var n=this;this.backend||this.logger.warn("No backend was added via i18next.use. Will not load resources."),"string"==typeof t&&(t=this.languageUtils.toResolveHierarchy(t)),"string"==typeof e&&(e=[e]),t.forEach(function(t){e.forEach(function(e){n.loadOne(t+"|"+e,"re")})})},e.prototype.loadOne=function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=t.split("|"),r=x(o,2),i=r[0],s=r[1];this.read(i,s,"read",null,null,function(o,r){o&&e.logger.warn(n+"loading namespace "+s+" for language "+i+" failed",o),!o&&r&&e.logger.log(n+"loaded namespace "+s+" for language "+i,r),e.loaded(t,o,r)})},e.prototype.saveMissing=function(t,e,n,o,r){var i=arguments.length>5&&void 0!==arguments[5]?arguments[5]:{};this.backend&&this.backend.create&&this.backend.create(t,e,n,o,null,v({},i,{isUpdate:r})),t&&t[0]&&this.store.addResource(t[0],e,n,o)},e}(R),H=function(t){function e(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments[1];y(this,e);var r=b(this,t.call(this));if(r.options=h(n),r.services={},r.logger=O,r.modules={external:[]},o&&!r.isInitialized&&!n.isClone){var i;if(!r.options.initImmediate)return i=r.init(n,o),b(r,i);setTimeout(function(){r.init(n,o)},0)}return r}return m(e,t),e.prototype.init=function(){function t(t){return t?"function"==typeof t?new t:t:null}var e=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments[1];if("function"==typeof n&&(o=n,n={}),this.options=v({},f(),this.options,h(n)),this.format=this.options.interpolation.format,o||(o=g),!this.options.isClone){this.modules.logger?O.init(t(this.modules.logger),this.options):O.init(null,this.options);var r=new E(this.options);this.store=new N(this.options.resources,this.options);var i=this.services;i.logger=O,i.resourceStore=this.store,i.languageUtils=r,i.pluralResolver=new A(r,{prepend:this.options.pluralSeparator,compatibilityJSON:this.options.compatibilityJSON,simplifyPluralSuffix:this.options.simplifyPluralSuffix}),i.interpolator=new U(this.options),i.backendConnector=new T(t(this.modules.backend),i.resourceStore,i,this.options),i.backendConnector.on("*",function(t){for(var n=arguments.length,o=Array(n>1?n-1:0),r=1;r1?n-1:0),r=1;r0&&void 0!==arguments[0]?arguments[0]:g;if(this.options.resources)e(null);else{if(this.language&&"cimode"===this.language.toLowerCase())return e();var n=[],o=function(e){if(e){var o=t.services.languageUtils.toResolveHierarchy(e);o.forEach(function(t){n.indexOf(t)<0&&n.push(t)})}};if(this.language)o(this.language);else{var r=this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);r.forEach(function(t){return o(t)})}this.options.preload&&this.options.preload.forEach(function(t){return o(t)}),this.services.backendConnector.load(n,this.options.ns,e)}},e.prototype.reloadResources=function(t,e){t||(t=this.languages),e||(e=this.options.ns),this.services.backendConnector.reload(t,e)},e.prototype.use=function(t){return"backend"===t.type&&(this.modules.backend=t),("logger"===t.type||t.log&&t.warn&&t.error)&&(this.modules.logger=t),"languageDetector"===t.type&&(this.modules.languageDetector=t),"i18nFormat"===t.type&&(this.modules.i18nFormat=t),"postProcessor"===t.type&&C.addPostProcessor(t),"3rdParty"===t.type&&this.modules.external.push(t),this},e.prototype.changeLanguage=function(t,e){var n=this,o=function(t,o){n.translator.changeLanguage(o),o&&(n.emit("languageChanged",o),n.logger.log("languageChanged",o)),e&&e(t,function(){return n.t.apply(n,arguments)})},r=function(t){t&&(n.language=t,n.languages=n.services.languageUtils.toResolveHierarchy(t),n.translator.language||n.translator.changeLanguage(t),n.services.languageDetector&&n.services.languageDetector.cacheUserLanguage(t)),n.loadResources(function(e){o(e,t)})};t||!this.services.languageDetector||this.services.languageDetector.async?!t&&this.services.languageDetector&&this.services.languageDetector.async?this.services.languageDetector.detect(r):r(t):r(this.services.languageDetector.detect())},e.prototype.getFixedT=function(t,e){var n=this,o=function t(e,o){for(var r=arguments.length,i=Array(r>2?r-2:0),s=2;s0?this.languages[0]:this.language), +!t)return"rtl";var e=["ar","shu","sqr","ssh","xaa","yhd","yud","aao","abh","abv","acm","acq","acw","acx","acy","adf","ads","aeb","aec","afb","ajp","apc","apd","arb","arq","ars","ary","arz","auz","avl","ayh","ayl","ayn","ayp","bbz","pga","he","iw","ps","pbt","pbu","pst","prp","prd","ur","ydd","yds","yih","ji","yi","hbo","men","xmn","fa","jpr","peo","pes","prs","dv","sam"];return e.indexOf(this.services.languageUtils.getLanguagePartFromCode(t))>=0?"rtl":"ltr"},e.prototype.createInstance=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments[1];return new e(t,n)},e.prototype.cloneInstance=function(){var t=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:g,r=v({},this.options,n,{isClone:!0}),i=new e(r),s=["store","services","language"];return s.forEach(function(e){i[e]=t[e]}),i.translator=new j(i.services,i.options),i.translator.on("*",function(t){for(var e=arguments.length,n=Array(e>1?e-1:0),o=1;oAnswer +
        {% csrf_token %} + {{ form.as_p }} + +
        +{% endblock %} diff --git a/anatomy/templates/anatomy/answer_question.html b/anatomy/templates/anatomy/answer_question.html new file mode 100644 index 00000000..2d8889b3 --- /dev/null +++ b/anatomy/templates/anatomy/answer_question.html @@ -0,0 +1,13 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +

        Question

        +

        {{ question.question_type.first }}

        + + +
        {% csrf_token %} + {{ form.as_p }} + + +
        +{% endblock %} diff --git a/anatomy/templates/anatomy/base.html b/anatomy/templates/anatomy/base.html new file mode 100644 index 00000000..52eb267b --- /dev/null +++ b/anatomy/templates/anatomy/base.html @@ -0,0 +1,32 @@ +{% load static %} + + + + Anatomy Quiz + + + + + + + + + + + +
        + {% block navigation %} + {% endblock %} +
        +
        + {% block content %} + {% endblock %} +
        +
        +
        + + + \ No newline at end of file diff --git a/anatomy/templates/anatomy/exam.html b/anatomy/templates/anatomy/exam.html new file mode 100644 index 00000000..23500cb9 --- /dev/null +++ b/anatomy/templates/anatomy/exam.html @@ -0,0 +1,79 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} + + +

        Question {{question_details.current}} of {{question_details.total}}

        +

        {{ question.question_type.first }}

        + + + + + +
        +
        +
        + +
        {% csrf_token %} + {{ form.as_p }} + {% if question_details.current > 1 %} + + {% endif %} + {% if question_details.current >= question_details.total %} + + {% else %} + + {% endif %} + +
        +
        +
        + +
        +
        + + +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/exam_list.html b/anatomy/templates/anatomy/exam_list.html new file mode 100644 index 00000000..42e467ba --- /dev/null +++ b/anatomy/templates/anatomy/exam_list.html @@ -0,0 +1,9 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +{% for exam in exams %} + +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/exam_overview.html b/anatomy/templates/anatomy/exam_overview.html new file mode 100644 index 00000000..2ce96a79 --- /dev/null +++ b/anatomy/templates/anatomy/exam_overview.html @@ -0,0 +1,9 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +
        +

        {{ exam.name }}

        + This exam has {{question_number}} questions. +

        +
        +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/exam_scores.html b/anatomy/templates/anatomy/exam_scores.html new file mode 100644 index 00000000..a0571218 --- /dev/null +++ b/anatomy/templates/anatomy/exam_scores.html @@ -0,0 +1,40 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +
        +

        {{ exam.name }}

        + {% for user, value in user_answers_marks.items %} +

        Candidate: {{ user_names|get_item:user }}

        + + Answers: {% for ans, score in user_answers_and_marks|get_item:user %} + {{ans}} ({{score}}), + {% endfor %} +
        Total mark: {{ user_scores|get_item:user }} + {% endfor %} +
        +
        +

        Stats

        + Mean: {{mean}}, Median {{median}}, Mode {{mode}} +

        Distribution of results

        +
        {{plot|safe}}
        +
        +
        + + + + + + {% for user, value in user_answers_marks.items %} + + + + + {% endfor %} +
        Candidate IDScore
        {{user}}{{user_scores|get_item:user}}
        +
        +{% endblock %} + +{% block navigation %} +Exams Marking + overview Scores +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/index.html b/anatomy/templates/anatomy/index.html new file mode 100644 index 00000000..c77de4bd --- /dev/null +++ b/anatomy/templates/anatomy/index.html @@ -0,0 +1,11 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +{% for exam in exams %} +
        +

        Exam {{ forloop.counter }}

        + {% if request.user.is_staff %}Mark answers{% endif %} + {% if request.user.is_staff %}Scores{% endif %} +
        +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/mark.html b/anatomy/templates/anatomy/mark.html new file mode 100644 index 00000000..1eb2d650 --- /dev/null +++ b/anatomy/templates/anatomy/mark.html @@ -0,0 +1,47 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +

        Marking question {{question_details.current}} of {{question_details.total}}

        +

        {{ question.question_type }}

        + +
        {% csrf_token %} +
        Marked: +
          + {% for answer in question.answers.all %} +
        • {{ answer }}
        • + {% endfor %} + {% for answer in question.half_mark_answers.all %} +
        • {{ answer }}
        • + {% endfor %} + {% for answer in question.incorrect_answers.all %} +
        • {{ answer }}
        • + {% endfor %} +
        + Unmarked: +
          + {% for answer in user_answers %} +
        • {{ answer }}
        • + {% endfor %} +
        +
        Key: 2 Marks, 1 + Mark, 0 Marks
        + {% if question_details.current > 1 %} + + {% endif %} + + {% if question_details.current >= question_details.total %} + {% else %} + + {% endif %} +
        + + {{ form.as_p }} + +
        +
        +
        +{% endblock %} +{% block navigation %} +Exams Marking + overview Scores +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/mark_overview.html b/anatomy/templates/anatomy/mark_overview.html new file mode 100644 index 00000000..bd88cccb --- /dev/null +++ b/anatomy/templates/anatomy/mark_overview.html @@ -0,0 +1,20 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +
        +

        {{ exam.name }}

        + + +

        +
        +{% endblock %} + +{% block navigation %} +Exams Marking + overview Scores +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/question_detail.html b/anatomy/templates/anatomy/question_detail.html new file mode 100644 index 00000000..4e664969 --- /dev/null +++ b/anatomy/templates/anatomy/question_detail.html @@ -0,0 +1,14 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +
        +
        + {{ question.created_date }} +
        +

        {% for answer in question.answers.all %} + {{ answer }}, + {% endfor %}

        +

        {{ question.question_type.first|linebreaksbr }}

        + +
        +{% endblock %} \ No newline at end of file diff --git a/anatomy/templates/anatomy/question_list.html b/anatomy/templates/anatomy/question_list.html new file mode 100644 index 00000000..f981a180 --- /dev/null +++ b/anatomy/templates/anatomy/question_list.html @@ -0,0 +1,20 @@ +{% extends 'anatomy/base.html' %} + +{% block content %} +{% for question in questions %} +
        +
        + {{ question.created_date }} +
        +

        {% for answer in question.answers.all %} + {{ answer }}, + {% endfor %} +

        +

        {{ question.question_type.first|linebreaksbr }}

        + + ANSWER +
        +{% endfor %} +{% endblock %} + +{$ black navigation %} \ No newline at end of file diff --git a/anatomy/templates/registration/login.html b/anatomy/templates/registration/login.html new file mode 100644 index 00000000..f23f7ee2 --- /dev/null +++ b/anatomy/templates/registration/login.html @@ -0,0 +1,12 @@ +{% extends '../anatomy/base.html' %} + +{% block title %}Login{% endblock %} + +{% block content %} +

        Login

        +
        + {% csrf_token %} + {{ form.as_p }} + +
        +{% endblock %} diff --git a/anatomy/tests.py b/anatomy/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/anatomy/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/anatomy/urls.py b/anatomy/urls.py new file mode 100644 index 00000000..e0418016 --- /dev/null +++ b/anatomy/urls.py @@ -0,0 +1,20 @@ +from django.urls import path, include +from . import views + +app_name = "anatomy" + +urlpatterns = [ + # path('', views.question_list, name='question_list'), + path("", views.index, name="index"), + path("question//", views.question_detail, name="question_detail"), + path("question//answer/", views.answer_question, name="answer_question"), + path("exam///mark", views.mark, name="mark"), + path("exam//mark", views.mark_overview, name="mark_overview"), + path("exam///", views.exam, name="exam"), + path("exam//", views.exam_overview, name="exam_overview"), + path("exam//scores", views.exam_scores_cid, name="exam_scores_cid"), + path("exam//json", views.exam_json, name="exam_json"), + path("submit_answers", views.postExamAnswers, name="exam_answers_submit"), + path("exam/", views.exam_list, name="exam_list"), + path("ajax/exam/flag/", views.flag_question, name="flag_question"), +] diff --git a/anatomy/views.py b/anatomy/views.py new file mode 100644 index 00000000..bbaa6ef6 --- /dev/null +++ b/anatomy/views.py @@ -0,0 +1,530 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.views.decorators.csrf import csrf_exempt +from django import forms + +# from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.models import User + +from django.http import Http404, JsonResponse + +from .forms import AnatomyAnswerForm, MarkAnatomyQuestionForm +from .models import ( + AnatomyQuestion, + CidUserAnswer, + Exam, + Answers, + HalfMarkAnswers, + IncorrectAnswers, +) + +from collections import defaultdict +import os +import base64 +import mimetypes +import json +import statistics +import plotly.express as px + + +def image_as_base64(image_file): + """ + :param `image_file` for the complete path of image. + :param `format` is format for image, eg: `png` or `jpg`. + """ + # if not os.path.isfile(image_file): + # return None + + encoded_string = "" + # with open(image_file, 'rb') as img_f: + # encoded_string = base64.b64encode(img_f.read()) + encoded_string = base64.b64encode(image_file.file.read()) + mimetype, enc = mimetypes.guess_type(image_file.path) + return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8")) + + +from django.template.defaulttags import register + + +@register.filter +def get_item(dictionary, key): + return dictionary.get(key) + + +def user_is_admin(user): + if user: + if user.pk == 1: + return True + return False + + +def question_list(request): + questions = AnatomyQuestion.objects.all() + return render(request, "anatomy/question_list.html", {"questions": questions}) + + +@login_required +def index(request): + exams = Exam.objects.all() + return render(request, "anatomy/index.html", {"exams": exams}) + + +@login_required +def question_detail(request, pk): + question = get_object_or_404(AnatomyQuestion, pk=pk) + return render(request, "anatomy/question_detail.html", {"question": question}) + + +@login_required +def answer_question(request, pk): + question = get_object_or_404(AnatomyQuestion, pk=pk) + answer = question.user_answers.filter( + user=request.user + ).first() # .filter(user=User) + if request.method == "POST": + if answer: + form = AnatomyAnswerForm(request.POST, instance=answer) + else: + form = AnatomyAnswerForm(request.POST) + if form.is_valid(): + answer = form.save(commit=False) + answer.user = request.user + answer.question = question + # answer.published_date = timezone.now() + answer.save() + return redirect("question_detail", pk=pk) + else: + form = AnatomyAnswerForm(instance=answer) + return render( + request, "anatomy/answer_question.html", {"form": form, "question": question} + ) + + +@login_required +def exam_list(request): + exams = Exam.objects.all() + return render(request, "anatomy/exam_list.html", {"exams": exams}) + + +@login_required +def exam_overview(request, pk): + print(type(pk)) + print(Exam.objects.all()) + exams = Exam.objects.all() + print("test", Exam.objects.all().get(id=pk)) + exam = get_object_or_404(Exam, pk=pk) + print("exam", exam) + + questions = exam.exam_questions.all() + + question_number = len(questions) + + return render( + request, + "anatomy/exam_overview.html", + {"exam": exam, "questions": questions, "question_number": question_number}, + ) + + +@login_required +def exam(request, pk, sk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + try: + question = questions[sk] + except IndexError: + raise Http404("Exam question does not exist") + + n = sk + + question_details = { + "total": len(questions), + "current": n + 1, + } + + # Get data for flagged + # answered_questions = UserAnswer.objects.filter(user=request.user).values_list("question__pk", flat=True) + user_answer_data = UserAnswer.objects.filter(user=request.user).values_list( + "question__pk", "answer", "flagged" + ) + # flagged_questions = UserAnswer.objects.filter(user=request.user, flagged=True).values_list("question__pk", flat=True) + # flagged_questions I= UserAnswer.filter(user=request.user, ).values_list("question__pk", flat=True) + + answered_questions = set() + flagged_questions = set() + for ans_pk, answer, flagged in user_answer_data: + if len(answer.strip()) > 0: + answered_questions.add(ans_pk) + if flagged: + flagged_questions.add(ans_pk) + + # u = question.user_answers + answer = question.user_answers.filter( + user=request.user + ).first() # .filter(user=User) + if request.method == "POST": + if answer: + form = AnatomyAnswerForm(request.POST, instance=answer) + else: + form = AnatomyAnswerForm(request.POST) + if form.is_valid(): + answer = form.save(commit=False) + answer.user = request.user + answer.question = question + # answer.published_date = timezone.now() + answer.save() + if "next" in request.POST: + return redirect("exam", pk=pk, sk=n + 1) + elif "previous" in request.POST: + return redirect("exam", pk=pk, sk=n - 1) + else: + form = AnatomyAnswerForm(instance=answer) + return render( + request, + "anatomy/exam.html", + { + "exam": exam, + "form": form, + "question": question, + "question_details": question_details, + "questions": questions, + "flagged_questions": flagged_questions, + "answered_questions": answered_questions, + "answer": answer, + }, + ) + + +def flag_question(request): + pk = request.GET.get("pk", None) + ans = UserAnswer.objects.filter(user=request.user, question__pk=pk).first() + ans.flagged = not ans.flagged + ans.save() + data = {"flagged": ans.flagged} + return JsonResponse(data) + + +@csrf_exempt +def postExamAnswers(request): + if request.is_ajax and request.method == "POST": + + # horrible but it works + for k in request.POST.dict(): + for q in json.loads(k): + # As access is not restricted make sure the data appears valid + if (not isinstance(q["cid"], int)) or ( + not isinstance(q["eid"], int) or (not isinstance(q["ans"], str)) + ): + return JsonResponse({"success": False}) + + # The model should catch invalid data but this should be less intensive + max_int = 10000000 + if q["cid"] > max_int or q["eid"] > max_int: + return JsonResponse({"success": False}) + + exiting_answers = CidUserAnswer.objects.filter( + question__id=q["qid"], exam__id=q["eid"], cid=q["cid"] + ) + + if not exiting_answers: + ans = UserAnswer(answer=q["ans"], cid=q["cid"]) + ans.question_id = q["qid"] + ans.exam_id = q["eid"] + + ans.full_clean() + + ans.save() + else: + # Update an existing answer + # should never be more than one (famous last words) + ans = exiting_answers[0] + ans.answer = q["ans"] + ans.save() + + # print(UserAnswer.objects.filter(exam__id=q["eid"])) + # print(request.urlencode()) + + return JsonResponse({"success": True}) + + # return render(request, "anatomy/exam.html", {"exam" : exam, "question" : question}) + + +@login_required +@user_passes_test(user_is_admin, login_url="/accounts/login") +def mark_overview(request, pk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + return render( + request, "anatomy/mark_overview.html", {"exam": exam, "questions": questions} + ) + + +@login_required +@user_passes_test(user_is_admin, login_url="/accounts/login") +def mark(request, pk, sk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + n = sk + + question_details = { + "total": len(questions), + "current": n + 1, + } + + try: + question = questions[sk] + except IndexError: + raise Http404("Exam question does not exist") + + correct_answers = [i.answer for i in question.answers.all()] + half_correct_answers = [i.answer for i in question.half_mark_answers.all()] + incorrect_answers = [i.answer for i in question.incorrect_answers.all()] + + user_answers = ( + set([i.answer for i in question.user_answers.all()]) + - set(correct_answers) + - set(half_correct_answers) + - set(incorrect_answers) + ) # .filter(user=User) + user_answers = set([i for i in user_answers if i.strip() != ""]) + if request.method == "POST": + + form = MarkAnatomyQuestionForm(request.POST) + + if form.is_valid(): + cd = form.cleaned_data + # correct = cd.get("correct") + # half_correct = cd.get("half_correct") + # incorrect = cd.get("incorrect") + marked_answers = json.loads(cd.get("marked_answers")) + + # This is mildly dangerous as we delete all answers + question.answers.all().delete() + question.half_mark_answers.all().delete() + question.incorrect_answers.all().delete() + + # This should probably use json (or something else...) + # ajax..... + for ans in marked_answers["correct"]: + if ans.strip() == "": + continue + print("correct", ans) + print(question.pk) + a = Answers() + a.question_id = question.pk + a.answer = ans.strip() + a.save() + # for ans in half_correct.split("--//--"): + for ans in marked_answers["half-correct"]: + if ans.strip() == "": + continue + print("halfcorrect", ans) + a = HalfMarkAnswers() + a.question_id = question.pk + a.answer = ans.strip() + a.save() + for ans in marked_answers["incorrect"]: + if ans.strip() == "": + continue + print("incorrect", ans) + a = IncorrectAnswers() + a.question_id = question.pk + a.answer = ans.strip() + a.save() + # answer = form.save(commit=False) + # answer.user = request.user + # answer.question = question + # answer.published_date = timezone.now() + # answer.save() + if "next" in request.POST: + return redirect("mark", pk=pk, sk=n + 1) + elif "previous" in request.POST: + return redirect("mark", pk=pk, sk=n - 1) + else: + form = MarkAnatomyQuestionForm() + return render( + request, + "anatomy/mark.html", + { + "exam": exam, + "form": form, + "question": question, + "question_details": question_details, + "user_answers": user_answers, + }, + ) + + +def exam_json(request, pk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + exam_questions = defaultdict(dict) + + for q in questions: + exam_questions[q.id] = { + "title": "{}".format(q.examination), + "question": str(q.question_type), + "images": [image_as_base64(q.image)], + "type": "anatomy", + } + + exam_json = { + "eid": exam.id, + "exam_type": "anatomy", + "exam_name": exam.name, + "exam_mode": True, + "questions": exam_questions, + } + return JsonResponse(exam_json) + + +@login_required +def exam_scores_cid(request, pk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + cids = ( + UserAnswer.objects.filter(question__in=questions) + .values_list("cid", flat=True) + .distinct() + ) + + user_answers_and_marks = defaultdict(list) + user_answers_marks = defaultdict(list) + user_answers = defaultdict(list) + user_names = {} + + for cid in cids: + # Convoluted (probably...) + user_names[cid] = cid + for q in questions: + s = q.user_answers.filter(cid=cid) + if not s: + user_answers_marks[cid].append(0) + user_answers[cid].append("") + continue + + ans = s[0].answer + if ans in q.answers.all().values_list("answer", flat=True): + a = 2 + elif ans in q.half_mark_answers.all().values_list("answer", flat=True): + a = 1 + else: + a = 0 + user_answers[cid].append(ans) + user_answers_marks[cid].append(a) + user_answers_and_marks[cid].append((ans, a)) + + user_scores = {} + for user in user_answers_marks: + user_scores[user] = sum(user_answers_marks[user]) + + user_scores_list = list(user_scores.values()) + mean = statistics.mean(user_scores_list) + median = statistics.median(user_scores_list) + try: + mode = statistics.mode(user_scores_list) + except statistics.StatisticsError: + mode = "No unique mode" + + df = user_scores_list + fig = px.histogram(df, x=0) + fig_html = fig.to_html() + + total = len(questions) + + return render( + request, + "anatomy/exam_scores.html", + { + "exam": exam, + "questions": questions, + "user_answers": dict(user_answers), + "user_answers_marks": dict(user_answers_marks), + "user_scores": user_scores, + "user_names": user_names, + "user_answers_and_marks": user_answers_and_marks, + "mean": mean, + "median": median, + "mode": mode, + "plot": fig_html, + }, + ) + + +@login_required +def exam_scores(request, pk): + exam = get_object_or_404(Exam, pk=pk) + + questions = exam.exam_questions.all() + + users = ( + UserAnswer.objects.filter(question__in=questions).values_list("user").distinct() + ) + + user_answers_and_marks = defaultdict(list) + user_answers_marks = defaultdict(list) + user_answers = defaultdict(list) + user_names = {} + + for u in users: + # Convoluted (probably...) + user_names[u] = User.objects.filter(pk=u[0]).first().get_username() + for q in questions: + s = q.user_answers.filter(user__in=u) + if not s: + user_answers_marks[u].append(0) + user_answers[u].append("") + continue + + ans = s[0].answer + if ans in q.answers.all().values_list("answer", flat=True): + a = 2 + elif ans in q.half_mark_answers.all().values_list("answer", flat=True): + a = 1 + else: + a = 0 + user_answers[u].append(ans) + user_answers_marks[u].append(a) + user_answers_and_marks[u].append((ans, a)) + + user_scores = {} + for user in user_answers_marks: + user_scores[user] = sum(user_answers_marks[user]) + + # Users with answers + # + # for q in questions: + # answers = q.user_answers.all() + # + # for ans in answers: + # user = ans.user + # if ans.answer in q.answers.all().values_list("answer", flat=True): + # user_answers[user].append(2) + # + + total = len(questions) + + return render( + request, + "anatomy/exam_scores.html", + { + "exam": exam, + "questions": questions, + "user_answers": dict(user_answers), + "user_answers_marks": dict(user_answers_marks), + "user_scores": user_scores, + "user_names": user_names, + "user_answers_and_marks": user_answers_and_marks, + }, + ) + diff --git a/manage.py b/manage.py new file mode 100755 index 00000000..2583b5cf --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/rad/__init__.py b/rad/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rad/settings.py b/rad/settings.py new file mode 100644 index 00000000..3c0acf87 --- /dev/null +++ b/rad/settings.py @@ -0,0 +1,170 @@ +""" +Django settings for rad project. + +Generated by 'django-admin startproject' using Django 1.11.14. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.11/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +with open('/etc/secret_key') as f: + SECRET_KEY = f.read().strip() + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ["localhost", "127.0.0.1", "161.35.163.87"] + + +# Application definition + +INSTALLED_APPS = [ + 'anatomy', + #"tagulous", + "reversion", + "django_tables2", + "django_filters", + "easy_thumbnails", + "filer", + "mptt", + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "reversion.middleware.RevisionMiddleware", +] + +ROOT_URLCONF = 'rad.urls' + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.static", + ], + }, + }, +] + +WSGI_APPLICATION = 'rad.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.11/ref/settings/#databases + +DATABASES = { + 'default': { + "ENGINE": "django.db.backends.postgresql", + "NAME": "rad", + "USER": "django", + "PASSWORD": "f7bf31dc9bda1256ea827953480d1917", + #"HOST": "xkjq.uk", + "HOST": "161.35.163.87", + "PORT": "5432", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +SERIALIZATION_MODULES = { + "xml": "tagulous.serializers.xml_serializer", + "json": "tagulous.serializers.json", + "python": "tagulous.serializers.python", + "yaml": "tagulous.serializers.pyyaml", +} + +# Internationalization +# https://docs.djangoproject.com/en/1.11/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.11/howto/static-files/ + +STATIC_URL = '/static/' + + +# Redirect to home URL after login (Default redirects to /accounts/profile/) +LOGIN_REDIRECT_URL = "/" + +TAGULOUS_AUTOCOMPLETE_JS = ( + "tagulous/lib/jquery.js", + "tagulous/lib/select2-3/select2.min.js", + "tagulous/tagulous.js", + "tagulous/adaptor/select2-3.js", +) + +TAGULOUS_AUTOCOMPLETE_CSS = {"all": ["tagulous/lib/select2-3/select2.css"]} + +INTERNAL_IPS = ["localhost", "127.0.0.1"] + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "incremental": True, + "root": { + "level": "DEBUG", + }, +} + +CORS_ORIGIN_ALLOW_ALL = True diff --git a/rad/urls.py b/rad/urls.py new file mode 100644 index 00000000..4b98c252 --- /dev/null +++ b/rad/urls.py @@ -0,0 +1,45 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.conf import settings +from django.conf.urls.static import static +from django.views.generic import TemplateView + +urlpatterns = [ + path("admin/", admin.site.urls, name="admin"), + path("anatomy/", include("anatomy.urls"), name="anatomy"), + #path("sbas/", include("sbas.urls"), name="sbas"), + #path("rapids/", include("rapids.urls"), name="rapids"), + path("accounts/", include("django.contrib.auth.urls")), + path("", TemplateView.as_view(template_name="index.html"), name="home"), + +] + +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, + document_root=settings.STATIC_ROOT) + import debug_toolbar + + urlpatterns = [ + path("__debug__/", include(debug_toolbar.urls)), + # For django versions before 2.0: + # url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/rad/wsgi.py b/rad/wsgi.py new file mode 100644 index 00000000..23078214 --- /dev/null +++ b/rad/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for rad project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rad.settings") + +application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..90376ccf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +Django +django_debug_toolbar +django_jquery +django_reversion +django_tagulous +Pillow +django-cors-headers +plotly +pandas +django-tables2 +django-filter +django-filer +psycopg2-binary \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..f1c14139 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,28 @@ +{% load static %} + + + + Radiology Stuff + + + + + + + +
        + {% block navigation %} + {% endblock %} +
        +
        + {% block content %} + {% endblock %} +
        +
        +
        + + + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..9636c154 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block content %} +
        +

        Radiology Stuff

        + Anatomy + Log in + {% if request.user.is_staff %}Admin{% endif %} + Reset password +
        +{% endblock %} \ No newline at end of file