PageSourceSearch

https://viomedi.de/assets/scripts/dwv.js

js viomedi.de collected 2026-10-06 14:17:21 UTC 1,004,381 bytes, 25,141 lines download raw bytes

1// Inspired from umdjs
2// See https://github.com/umdjs/umd/blob/master/templates/returnExports.js
3(function(root, factory) {
4    if (typeof define === 'function' && define.amd) {
5        // AMD. Register as an anonymous module.
6        define([
7            'modernizr',
8            'i18next',
9            'i18nextXHRBackend',
10            'i18nextBrowserLanguageDetector',
11            'jszip',
12            'konva',
13            ''
14        ], factory);
15    } else if (typeof module === 'object' && module.exports) {
16        // Node. Does not work with strict CommonJS, but
17        // only CommonJS-like environments that support module.exports,
18        // like Node.
19
20        // i18next-xhr-backend: requires XMlHttpRequest
21        // Konva: requires 'canvas' -> deactivated for now...
22        // MagicWand: no package -> deactivated
23
24        module.exports = factory(
25            require('modernizr'),
26            require('i18next'),
27            require('i18next-xhr-backend'),
28            require('i18next-browser-languagedetector'),
29            require('jszip'),
30            null,
31            null
32        );
33    } else {
34        // Browser globals (root is window)
35        root.dwv = factory(
36            root.Modernizr,
37            root.i18next,
38            root.i18nextXHRBackend,
39            root.i18nextBrowserLanguageDetector,
40            root.JSZip,
41            root.Konva,
42            root.MagicWand
43        );
44    }
45}(this, function(
46    Modernizr,
47    i18next,
48    i18nextXHRBackend,
49    i18nextBrowserLanguageDetector,
50    JSZip,
51    Konva,
52    MagicWand) {
53
54    // similar to what browserify does but reversed
55    //https://www.contentful.com/blog/2017/01/17/the-global-object-in-javascript/
56    var window = typeof window !== 'undefined' ?
57        window : typeof self !== 'undefined' ?
58        self : typeof global !== 'undefined' ?
59        global : {};
60
61    /** @namespace */
62    var dwv = dwv || {};
63
64    /**
65     * Main application class.
66     * @constructor
67     */
68    dwv.App = function() {
69        // Local object
70        var self = this;
71
72        // Image
73        var image = null;
74        // Original image
75        var originalImage = null;
76        // Image data array
77        var imageData = null;
78        // Image data width
79        var dataWidth = 0;
80        // Image data height
81        var dataHeight = 0;
82        // Is the data mono-slice?
83        var isMonoSliceData = 0;
84
85        // Default character set
86        var defaultCharacterSet;
87
88        // Container div id
89        var containerDivId = null;
90        // Display window scale
91        var windowScale = 1;
92        // Fit display to window flag
93        var fitToWindow = false;
94        // main scale
95        var scale = 1;
96        // zoom center
97        var scaleCenter = { "x": 0, "y": 0 };
98        // translation
99        var translation = { "x": 0, "y": 0 };
100
101        // View
102        var view = null;
103        // View controller
104        var viewController = null;
105
106        // Info layer controller
107        var infoController = null;
108
109        // Dicom tags gui
110        var tagsGui = null;
111
112        // Drawing list gui
113        var drawListGui = null;
114
115        // Image layer
116        var imageLayer = null;
117
118        // Draw controller
119        var drawController = null;
120
121        // Generic style
122        var style = new dwv.html.Style();
123
124        // Toolbox controller
125        var toolboxController = null;
126
127        // Loadbox
128        var loadbox = null;
129        // Current loader
130        var currentLoader = null;
131
132        // UndoStack
133        var undoStack = null;
134
135        // listeners
136        var listeners = {};
137
138        /**
139         * Get the image.
140         * @return {Image} The associated image.
141         */
142        this.getImage = function() { return image; };
143        /**
144         * Set the view.
145         * @param {Image} img The associated image.
146         */
147        this.setImage = function(img) {
148            image = img;
149            view.setImage(img);
150        };
151        /**
152         * Restore the original image.
153         */
154        this.restoreOriginalImage = function() {
155            image = originalImage;
156            view.setImage(originalImage);
157        };
158        /**
159         * Get the image data array.
160         * @return {Array} The image data array.
161         */
162        this.getImageData = function() { return imageData; };
163        /**
164         * Is the data mono-slice?
165         * @return {Boolean} True if the data is mono-slice.
166         */
167        this.isMonoSliceData = function() { return isMonoSliceData; };
168
169        /**
170         * Get the main scale.
171         * @return {Number} The main scale.
172         */
173        this.getScale = function() { return scale / windowScale; };
174
175        /**
176         * Get the window scale.
177         * @return {Number} The window scale.
178         */
179        this.getWindowScale = function() { return windowScale; };
180
181        /**
182         * Get the scale center.
183         * @return {Object} The coordinates of the scale center.
184         */
185        this.getScaleCenter = function() { return scaleCenter; };
186
187        /**
188         * Get the translation.
189         * @return {Object} The translation.
190         */
191        this.getTranslation = function() { return translation; };
192
193        /**
194         * Get the view controller.
195         * @return {Object} The controller.
196         */
197        this.getViewController = function() { return viewController; };
198
199        /**
200         * Get the image layer.
201         * @return {Object} The image layer.
202         */
203        this.getImageLayer = function() { return imageLayer; };
204        /**
205         * Get the current draw layer.
206         * @return {Object} The draw layer.
207         */
208        this.getCurrentDrawLayer = function() {
209            return drawController.getCurrentDrawLayer();
210        };
211        /**
212         * Get the draw stage.
213         * @return {Object} The draw stage.
214         */
215        this.getDrawStage = function() {
216            return drawController.getDrawStage();
217        };
218
219        /**
220         * Get the app style.
221         * @return {Object} The app style.
222         */
223        this.getStyle = function() { return style; };
224
225        /**
226         * Add a command to the undo stack.
227         * @param {Object} The command to add.
228         */
229        this.addToUndoStack = function(cmd) {
230            if (undoStack !== null) {
231                undoStack.add(cmd);
232            }
233        };
234
235        /**
236         * Initialise the HTML for the application.
237         */
238        this.init = function(config) {
239            containerDivId = config.containerDivId;
240            // tools
241            if (config.tools && config.tools.length !== 0) {
242                // setup the tool list
243                var toolList = {};
244                for (var t = 0; t < config.tools.length; ++t) {
245                    var toolName = config.tools[t];
246                    if (toolName === "Draw") {
247                        if (config.shapes !== 0) {
248                            // setup the shape list
249                            var shapeList = {};
250                            for (var s = 0; s < config.shapes.length; ++s) {
251                                var shapeName = config.shapes[s];
252                                var shapeFactoryClass = shapeName + "Factory";
253                                if (typeof dwv.tool[shapeFactoryClass] !== "undefined") {
254                                    shapeList[shapeName] = dwv.tool[shapeFactoryClass];
255                                } else {
256                                    console.warn("Could not initialise unknown shape: " + shapeName);
257                                }
258                            }
259                            toolList.Draw = new dwv.tool.Draw(this, shapeList);
260                            toolList.Draw.addEventListener("draw-create", fireEvent);
261                            toolList.Draw.addEventListener("draw-change", fireEvent);
262                            toolList.Draw.addEventListener("draw-move", fireEvent);
263                            toolList.Draw.addEventListener("draw-delete", fireEvent);
264                        }
265                    } else if (toolName === "Filter") {
266                        if (config.filters.length !== 0) {
267                            // setup the filter list
268                            var filterList = {};
269                            for (var f = 0; f < config.filters.length; ++f) {
270                                var filterName = config.filters[f];
271                                if (typeof dwv.tool.filter[filterName] !== "undefined") {
272                                    filterList[filterName] = new dwv.tool.filter[filterName](this);
273                                } else {
274                                    console.warn("Could not initialise unknown filter: " + filterName);
275                                }
276                            }
277                            toolList.Filter = new dwv.tool.Filter(filterList, this);
278                            toolList.Filter.addEventListener("filter-run", fireEvent);
279                            toolList.Filter.addEventListener("filter-undo", fireEvent);
280                        }
281                    } else {
282                        // default: find the tool in the dwv.tool namespace
283                        var toolClass = toolName;
284                        if (typeof dwv.tool[toolClass] !== "undefined") {
285                            toolList[toolClass] = new dwv.tool[toolClass](this);
286                            if (typeof toolList[toolClass].addEventListener !== "undefined") {
287                                toolList[toolClass].addEventListener(fireEvent);
288                            }
289                        } else {
290                            console.warn("Could not initialise unknown tool: " + toolName);
291                        }
292                    }
293                }
294                toolboxController = new dwv.ToolboxController();
295                toolboxController.create(toolList, this);
296            }
297            // gui
298            if (config.gui) {
299                // tools
300                if (config.gui.indexOf("tool") !== -1 && toolboxController) {
301                    toolboxController.setup();
302                }
303                // load
304                if (config.gui.indexOf("load") !== -1) {
305                    var loaderList = {};
306                    for (var l = 0; l < config.loaders.length; ++l) {
307                        var loaderName = config.loaders[l];
308                        var loaderClass = loaderName + "Load";
309                        // default: find the loader in the dwv.gui namespace
310                        if (typeof dwv.gui[loaderClass] !== "undefined") {
311                            loaderList[loaderName] = dwv.gui[loaderClass](this);
312                        } else {
313                            console.warn("Could not initialise unknown loader: " + loaderName);
314                        }
315                    }
316                    loadbox = dwv.gui.Loadbox(this, loaderList);
317                    loadbox.setup();
318                    var loaderKeys = Object.keys(loaderList);
319                    for (var lk = 0; lk < loaderKeys.length; ++lk) {
320                        loaderList[loaderKeys[lk]].setup();
321                    }
322                    loadbox.displayLoader(loaderKeys[0]);
323                }
324                // undo
325                if (config.gui.indexOf("undo") !== -1) {
326                    undoStack = new dwv.tool.UndoStack(this);
327                    undoStack.setup();
328                }
329                // DICOM Tags
330                if (config.gui.indexOf("tags") !== -1) {
331                    tagsGui = new dwv.gui.DicomTags(this);
332                }
333                // Draw list
334                if (config.gui.indexOf("drawList") !== -1) {
335                    drawListGui = new dwv.gui.DrawList(this);
336                    // update list on draw events
337                    this.addEventListener("draw-create", drawListGui.update);
338                    this.addEventListener("draw-change", drawListGui.update);
339                    this.addEventListener("draw-delete", drawListGui.update);
340                }
341                // version number
342                if (config.gui.indexOf("version") !== -1) {
343                    dwv.gui.appendVersionHtml(dwv.getVersion());
344                }
345                // help
346                if (config.gui.indexOf("help") !== -1) {
347                    var isMobile = true;
348                    if (config.isMobile !== "undefined") {
349                        isMobile = config.isMobile;
350                    }
351                    dwv.gui.appendHelpHtml(toolboxController.getToolList(), isMobile, this);
352                }
353            }
354
355            // listen to drag&drop
356            var box = this.getElement("dropBox");
357            if (box) {
358                box.addEventListener("dragover", onDragOver);
359                box.addEventListener("dragleave", onDragLeave);
360                box.addEventListener("drop", onDrop);
361                // initial size
362                var size = dwv.gui.getWindowSize();
363                var dropBoxSize = 2 * size.height / 3;
364                box.setAttribute("style", "width:" + dropBoxSize + "px;height:" + dropBoxSize + "px");
365            }
366
367            // possible load from URL
368            if (typeof config.skipLoadUrl === "undefined") {
369                var query = dwv.utils.getUriQuery(window.location.href);
370                // check query
371                if (query && typeof query.input !== "undefined") {
372                    dwv.utils.decodeQuery(query, this.onInputURLs);
373                    // optional display state
374                    if (typeof query.state !== "undefined") {
375                        var onLoadEnd = function( /*event*/ ) {
376                            loadStateUrl(query.state);
377                        };
378                        this.addEventListener("load-end", onLoadEnd);
379                    }
380                }
381            } else {
382                console.log("Not loading url from address since skipLoadUrl is defined.");
383            }
384
385            // align layers when the window is resized
386            if (config.fitToWindow) {
387                fitToWindow = true;
388                window.onresize = this.onResize;
389            }
390
391            // default character set
392            if (typeof config.defaultCharacterSet !== "undefined") {
393                defaultCharacterSet = config.defaultCharacterSet;
394            }
395        };
396
397        /**
398         * Get a HTML element associated to the application.
399         * @param name The name or id to find.
400         * @return The found element or null.
401         */
402        this.getElement = function(name) {
403            return dwv.gui.getElement(containerDivId, name);
404        };
405
406        /**
407         * Reset the application.
408         */
409        this.reset = function() {
410            // clear tools
411            if (toolboxController) {
412                toolboxController.reset();
413            }
414            // clear draw
415            if (drawController) {
416                drawController.reset();
417            }
418            // clear objects
419            image = null;
420            view = null;
421            isMonoSliceData = false;
422            // reset undo/redo
423            if (undoStack) {
424                undoStack = new dwv.tool.UndoStack(this);
425                undoStack.initialise();
426            }
427        };
428
429        /**
430         * Reset the layout of the application.
431         */
432        this.resetLayout = function() {
433            var previousScale = scale;
434            var previousSC = scaleCenter;
435            var previousTrans = translation;
436            // reset values
437            scale = windowScale;
438            scaleCenter = { "x": 0, "y": 0 };
439            translation = { "x": 0, "y": 0 };
440            // apply new values
441            if (imageLayer) {
442                imageLayer.resetLayout(windowScale);
443                imageLayer.draw();
444            }
445            if (drawController) {
446                drawController.resetStage(windowScale);
447            }
448            // fire events
449            if (previousScale != scale) {
450                fireEvent({ "type": "zoom-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y });
451            }
452            if ((previousSC.x !== scaleCenter.x || previousSC.y !== scaleCenter.y) ||
453                (previousTrans.x !== translation.x || previousTrans.y !== translation.y)) {
454                fireEvent({ "type": "offset-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y });
455            }
456        };
457
458        /**
459         * Add an event listener on the app.
460         * @param {String} type The event type.
461         * @param {Object} listener The method associated with the provided event type.
462         */
463        this.addEventListener = function(type, listener) {
464            if (typeof listeners[type] === "undefined") {
465                listeners[type] = [];
466            }
467            listeners[type].push(listener);
468        };
469
470        /**
471         * Remove an event listener from the app.
472         * @param {String} type The event type.
473         * @param {Object} listener The method associated with the provided event type.
474         */
475        this.removeEventListener = function(type, listener) {
476            if (typeof listeners[type] === "undefined") {
477                return;
478            }
479            for (var i = 0; i < listeners[type].length; ++i) {
480                if (listeners[type][i] === listener) {
481                    listeners[type].splice(i, 1);
482                }
483            }
484        };
485
486        /**
487         * Load a list of files. Can be image files or a state file.
488         * @param {Array} files The list of files to load.
489         */
490        this.loadFiles = function(files) {
491            // has been checked for emptiness.
492            var ext = files[0].name.split('.').pop().toLowerCase();
493            if (ext === "json") {
494                loadStateFile(files[0]);
495            } else {
496                loadImageFiles(files);
497            }
498        };
499
500        /**
501         * Load a list of image files.
502         * @private
503         * @param {Array} files The list of image files to load.
504         */
505        function loadImageFiles(files) {
506            // create IO
507            var fileIO = new dwv.io.FilesLoader();
508            // load data
509            loadImageData(files, fileIO);
510        }
511
512        /**
513         * Load a State file.
514         * @private
515         * @param {String} file The state file to load.
516         */
517        function loadStateFile(file) {
518            // create IO
519            var fileIO = new dwv.io.FilesLoader();
520            // load data
521            loadStateData([file], fileIO);
522        }
523
524        /**
525         * Load a list of URLs. Can be image files or a state file.
526         * @param {Array} urls The list of urls to load.
527         * @param {Array} requestHeaders An array of {name, value} to use as request headers.
528         */
529        this.loadURLs = function(urls, requestHeaders) {
530            // has been checked for emptiness.
531            var ext = urls[0].split('.').pop().toLowerCase();
532            if (ext === "json") {
533                loadStateUrl(urls[0], requestHeaders);
534            } else {
535                loadImageUrls(urls, requestHeaders);
536            }
537        };
538
539        /**
540         * Abort the current load.
541         */
542        this.abortLoad = function() {
543            if (currentLoader) {
544                currentLoader.abort();
545                currentLoader = null;
546            }
547        };
548
549        /**
550         * Load a list of ArrayBuffers.
551         * @param {Array} data The list of ArrayBuffers to load
552         *   in the form of [{name: "", filename: "", data: data}].
553         */
554        this.loadImageObject = function(data) {
555            // create IO
556            var memoryIO = new dwv.io.MemoryLoader();
557            // create options
558            var options = {};
559            // load data
560            loadImageData(data, memoryIO, options);
561        };
562
563        /**
564         * Load a list of image URLs.
565         * @private
566         * @param {Array} urls The list of urls to load.
567         * @param {Array} requestHeaders An array of {name, value} to use as request headers.
568         */
569        function loadImageUrls(urls, requestHeaders) {
570            // create IO
571            var urlIO = new dwv.io.UrlsLoader();
572            // create options
573            var options = { 'requestHeaders': requestHeaders };
574            // load data
575            loadImageData(urls, urlIO, options);
576        }
577
578        /**
579         * Load a State url.
580         * @private
581         * @param {String} url The state url to load.
582         * @param {Array} requestHeaders An array of {name, value} to use as request headers.
583         */
584        function loadStateUrl(url, requestHeaders) {
585            // create IO
586            var urlIO = new dwv.io.UrlsLoader();
587            // create options
588            var options = { 'requestHeaders': requestHeaders };
589            // load data
590            loadStateData([url], urlIO, options);
591        }
592
593        /**
594         * Load a list of image data.
595         * @private
596         * @param {Array} data Array of data to load.
597         * @param {Object} loader The data loader.
598         * @param {Object} options Options passed to the final loader.
599         */
600        function loadImageData(data, loader, options) {
601            // store loader
602            currentLoader = loader;
603
604            // allow to cancel
605            var previousOnKeyDown = window.onkeydown;
606            window.onkeydown = function(event) {
607                if (event.ctrlKey && event.keyCode === 88) // crtl-x
608                {
609                    console.log("crtl-x pressed!");
610                    self.abortLoad();
611                }
612            };
613
614            // clear variables
615            self.reset();
616            // first data name
617            var firstName = "";
618            if (typeof data[0].name !== "undefined") {
619                firstName = data[0].name;
620            } else {
621                firstName = data[0];
622            }
623            // flag used by scroll to decide wether to activate or not
624            // TODO: supposing multi-slice for zip files, could not be...
625            isMonoSliceData = (data.length === 1 &&
626                firstName.split('.').pop().toLowerCase() !== "zip");
627            // set IO
628            loader.setDefaultCharacterSet(defaultCharacterSet);
629            loader.onload = function(data) {
630                if (image) {
631                    view.append(data.view);
632                    if (drawController) {
633                        drawController.appendDrawLayer(image.getNumberOfFrames());
634                    }
635                }
636                postLoadInit(data);
637            };
638            loader.onerror = function(error) { handleError(error); };
639            loader.onabort = function(error) { handleAbort(error); };
640            loader.onloadend = function( /*event*/ ) {
641                window.onkeydown = previousOnKeyDown;
642                if (drawController) {
643                    drawController.activateDrawLayer(viewController);
644                }
645                fireEvent({
646                    type: "load-progress",
647                    lengthComputable: true,
648                    loaded: 100,
649                    total: 100
650                });
651                fireEvent({ 'type': 'load-end' });
652                // reset member
653                currentLoader = null;
654            };
655            loader.onprogress = onLoadProgress;
656            // main load (asynchronous)
657            fireEvent({ 'type': 'load-start' });
658            loader.load(data, options);
659        }
660
661        /**
662         * Load a State data.
663         * @private
664         * @param {Array} data Array of data to load.
665         * @param {Object} loader The data loader.
666         * @param {Object} options Options passed to the final loader.
667         */
668        function loadStateData(data, loader, options) {
669            // set IO
670            loader.onload = function(data) {
671                // load state
672                var state = new dwv.State(self);
673                state.fromJSON(data);
674            };
675            loader.onerror = function(error) { handleError(error); };
676            // main load (asynchronous)
677            loader.load(data, options);
678        }
679
680        /**
681         * Fit the display to the given size. To be called once the image is loaded.
682         */
683        this.fitToSize = function(size) {
684            // previous width
685            var oldWidth = parseInt(windowScale * dataWidth, 10);
686            // find new best fit
687            windowScale = Math.min((size.width / dataWidth), (size.height / dataHeight));
688            // new sizes
689            var newWidth = parseInt(windowScale * dataWidth, 10);
690            var newHeight = parseInt(windowScale * dataHeight, 10);
691            // ratio previous/new to add to zoom
692            var mul = newWidth / oldWidth;
693            scale *= mul;
694
695            // update style
696            style.setScale(windowScale);
697
698            // resize container
699            var container = this.getElement("layerContainer");
700            container.setAttribute("style", "width:" + newWidth + "px;height:" + newHeight + "px");
701            // resize image layer
702            if (imageLayer) {
703                imageLayer.setWidth(newWidth);
704                imageLayer.setHeight(newHeight);
705                imageLayer.zoom(scale, scale, 0, 0);
706                imageLayer.draw();
707            }
708            // resize draw stage
709            if (drawController) {
710                drawController.resizeStage(newWidth, newHeight, scale);
711            }
712        };
713
714        /**
715         * Toggle the display of the information layer.
716         */
717        this.toggleInfoLayerDisplay = function() {
718            // toggle html
719            var infoLayer = self.getElement("infoLayer");
720            dwv.html.toggleDisplay(infoLayer);
721            // toggle listeners
722            infoController.toggleListeners(self, view);
723        };
724
725        /**
726         * Init the Window/Level display
727         */
728        this.initWLDisplay = function() {
729            // set window/level to first preset
730            viewController.setWindowLevelPresetById(0);
731            // default position
732            viewController.setCurrentPosition2D(0, 0);
733            // default frame
734            viewController.setCurrentFrame(0);
735        };
736
737        /**
738         * Add canvas mouse and touch listeners.
739         * @param {Object} canvas The canvas to listen to.
740         */
741        this.addToolCanvasListeners = function(layer) {
742            toolboxController.addCanvasListeners(layer);
743        };
744
745        /**
746         * Remove layer mouse and touch listeners.
747         * @param {Object} canvas The canvas to stop listening to.
748         */
749        this.removeToolCanvasListeners = function(layer) {
750            toolboxController.removeCanvasListeners(layer);
751        };
752
753        /**
754         * Render the current image.
755         */
756        this.render = function() {
757            generateAndDrawImage();
758        };
759
760        /**
761         * Zoom to the layers.
762         * @param {Number} zoom The zoom to apply.
763         * @param {Number} cx The zoom center X coordinate.
764         * @param {Number} cy The zoom center Y coordinate.
765         */
766        this.zoom = function(zoom, cx, cy) {
767            scale = zoom * windowScale;
768            if (scale <= 0.1) {
769                scale = 0.1;
770            }
771            scaleCenter = { "x": cx, "y": cy };
772            zoomLayers();
773        };
774
775        /**
776         * Add a step to the layers zoom.
777         * @param {Number} step The zoom step increment. A good step is of 0.1.
778         * @param {Number} cx The zoom center X coordinate.
779         * @param {Number} cy The zoom center Y coordinate.
780         */
781        this.stepZoom = function(step, cx, cy) {
782            scale += step;
783            if (scale <= 0.1) {
784                scale = 0.1;
785            }
786            scaleCenter = { "x": cx, "y": cy };
787            zoomLayers();
788        };
789
790        /**
791         * Apply a translation to the layers.
792         * @param {Number} tx The translation along X.
793         * @param {Number} ty The translation along Y.
794         */
795        this.translate = function(tx, ty) {
796            translation = { "x": tx, "y": ty };
797            translateLayers();
798        };
799
800        /**
801         * Add a translation to the layers.
802         * @param {Number} tx The step translation along X.
803         * @param {Number} ty The step translation along Y.
804         */
805        this.stepTranslate = function(tx, ty) {
806            var txx = translation.x + tx / scale;
807            var tyy = translation.y + ty / scale;
808            translation = { "x": txx, "y": tyy };
809            translateLayers();
810        };
811
812        /**
813         * Get the list of drawing display details.
814         * @return {Object} The list of draw details including id, slice, frame...
815         */
816        this.getDrawDisplayDetails = function() {
817            return drawController.getDrawDisplayDetails();
818        };
819        /**
820         * Get the list of drawings.
821         * @return {Object} The list of drawings.
822         */
823        this.getDraws = function() {
824            return drawController.getDraws();
825        };
826        /**
827         * Get a list of drawing store details.
828         * @return {Object} A list of draw details including id, text, quant...
829         */
830        this.getDrawStoreDetails = function() {
831            return drawController.getDrawStoreDetails();
832        };
833        /**
834         * Set the drawings on the current stage.
835         * @param {Array} drawings An array of drawings.
836         * @param {Array} drawingsDetails An array of drawings details.
837         */
838        this.setDrawings = function(drawings, drawingsDetails) {
839            drawController.setDrawings(drawings, drawingsDetails, fireEvent, this.addToUndoStack);
840        };
841        /**
842         * Update a drawing from its details.
843         * @param {Object} drawDetails Details of the drawing to update.
844         */
845        this.updateDraw = function(drawDetails) {
846            drawController.updateDraw(drawDetails);
847        };
848        /**
849         * Delete all Draws from all layers.
850         */
851        this.deleteDraws = function() {
852            drawController.deleteDraws(fireEvent, this.addToUndoStack);
853        };
854        /**
855         * Check the visibility of a given group.
856         * @param {Object} drawDetails Details of the drawing to check.
857         */
858        this.isGroupVisible = function(drawDetails) {
859            return drawController.isGroupVisible(drawDetails);
860        };
861        /**
862         * Toggle group visibility.
863         * @param {Object} drawDetails Details of the drawing to update.
864         */
865        this.toogleGroupVisibility = function(drawDetails) {
866            drawController.toogleGroupVisibility(drawDetails);
867        };
868
869        // Handler Methods -----------------------------------------------------------
870
871        /**
872         * Handle window/level change.
873         * @param {Object} event The event fired when changing the window/level.
874         */
875        this.onWLChange = function(event) {
876            // generate and draw if no skip flag
877            if (typeof event.skipGenerate === "undefined" ||
878                event.skipGenerate === false) {
879                generateAndDrawImage();
880            }
881        };
882
883        /**
884         * Handle colour map change.
885         * @param {Object} event The event fired when changing the colour map.
886         */
887        this.onColourChange = function( /*event*/ ) {
888            generateAndDrawImage();
889        };
890
891        /**
892         * Handle frame change.
893         * @param {Object} event The event fired when changing the frame.
894         */
895        this.onFrameChange = function( /*event*/ ) {
896            generateAndDrawImage();
897            if (drawController) {
898                drawController.activateDrawLayer(viewController);
899            }
900        };
901
902        /**
903         * Handle slice change.
904         * @param {Object} event The event fired when changing the slice.
905         */
906        this.onSliceChange = function( /*event*/ ) {
907            generateAndDrawImage();
908            if (drawController) {
909                drawController.activateDrawLayer(viewController);
910            }
911        };
912
913        /**
914         * Handle key down event.
915         * - CRTL-Z: undo
916         * - CRTL-Y: redo
917         * - CRTL-ARROW_LEFT: next frame
918         * - CRTL-ARROW_UP: next slice
919         * - CRTL-ARROW_RIGHT: previous frame
920         * - CRTL-ARROW_DOWN: previous slice
921         * Default behavior. Usually used in tools.
922         * @param {Object} event The key down event.
923         */
924        this.onKeydown = function(event) {
925            if (event.ctrlKey) {
926                if (event.keyCode === 37) // crtl-arrow-left
927                {
928                    event.preventDefault();
929                    self.getViewController().decrementFrameNb();
930                } else if (event.keyCode === 38) // crtl-arrow-up
931                {
932                    event.preventDefault();
933                    self.getViewController().incrementSliceNb();
934                } else if (event.keyCode === 39) // crtl-arrow-right
935                {
936                    event.preventDefault();
937                    self.getViewController().incrementFrameNb();
938                } else if (event.keyCode === 40) // crtl-arrow-down
939                {
940                    event.preventDefault();
941                    self.getViewController().decrementSliceNb();
942                } else if (event.keyCode === 89) // crtl-y
943                {
944                    undoStack.redo();
945                } else if (event.keyCode === 90) // crtl-z
946                {
947                    undoStack.undo();
948                }
949            }
950        };
951
952        /**
953         * Handle resize.
954         * Fit the display to the window. To be called once the image is loaded.
955         * @param {Object} event The change event.
956         */
957        this.onResize = function( /*event*/ ) {
958            self.fitToSize(dwv.gui.getWindowSize());
959        };
960
961        /**
962         * Handle zoom reset.
963         * @param {Object} event The change event.
964         */
965        this.onZoomReset = function( /*event*/ ) {
966            self.resetLayout();
967        };
968
969        /**
970         * Handle loader change.
971         * @param {Object} event The change event.
972         */
973        this.onChangeLoader = function( /*event*/ ) {
974            // called from an HTML select, use its value
975            loadbox.displayLoader(this.value);
976        };
977
978        /**
979         * Reset the load box to its original state.
980         */
981        this.resetLoadbox = function() {
982            loadbox.reset();
983        };
984
985        /**
986         * Handle change url event.
987         * @param {Object} event The event fired when changing the url field.
988         */
989        this.onChangeURL = function(event) {
990            self.loadURLs([event.target.value]);
991        };
992
993        /**
994         * Handle input urls.
995         * @param {Array} urls The list of input urls.
996         * @param {Array} requestHeaders An array of {name, value} to use as request headers.
997         */
998        this.onInputURLs = function(urls, requestHeaders) {
999            self.loadURLs(urls, requestHeaders);
1000        };
1001
1002        /**
1003         * Handle change files event.
1004         * @param {Object} event The event fired when changing the file field.
1005         */
1006        this.onChangeFiles = function(event) {
1007            var files = event.target.files;
1008            if (files.length !== 0) {
1009                self.loadFiles(files);
1010            }
1011        };
1012
1013        /**
1014         * Handle state save event.
1015         * @param {Object} event The event fired when changing the state save field.
1016         */
1017        this.onStateSave = function( /*event*/ ) {
1018            var state = new dwv.State(self);
1019            // add href to link (html5)
1020            var element = self.getElement("download-state");
1021            element.href = "data:application/json," + state.toJSON();
1022        };
1023
1024        /**
1025         * Handle colour map change.
1026         * @param {Object} event The change event.
1027         */
1028        this.onChangeColourMap = function( /*event*/ ) {
1029            // called from an HTML select, use its value
1030            viewController.setColourMapFromName(this.value);
1031        };
1032
1033        /**
1034         * Handle window/level preset change.
1035         * @param {Object} event The change event.
1036         */
1037        this.onChangeWindowLevelPreset = function( /*event*/ ) {
1038            // value should be the name of the preset
1039            viewController.setWindowLevelPreset(this.value);
1040        };
1041
1042        /**
1043         * Handle tool change.
1044         * @param {Object} event The change event.
1045         */
1046        this.onChangeTool = function( /*event*/ ) {
1047            // called from an HTML select, use its value
1048            toolboxController.setSelectedTool(this.value);
1049        };
1050
1051        /**
1052         * Handle shape change.
1053         * @param {Object} event The change event.
1054         */
1055        this.onChangeShape = function( /*event*/ ) {
1056            // called from an HTML select, use its value
1057            toolboxController.setSelectedShape(this.value);
1058        };
1059
1060        /**
1061         * Handle filter change.
1062         * @param {Object} event The change event.
1063         */
1064        this.onChangeFilter = function( /*event*/ ) {
1065            // called from an HTML select, use its value
1066            toolboxController.setSelectedFilter(this.value);
1067        };
1068
1069        /**
1070         * Handle filter run.
1071         * @param {Object} event The run event.
1072         */
1073        this.onRunFilter = function( /*event*/ ) {
1074            toolboxController.runSelectedFilter();
1075        };
1076
1077        /**
1078         * Handle line colour change.
1079         * @param {Object} event The change event.
1080         */
1081        this.onChangeLineColour = function( /*event*/ ) {
1082            // called from an HTML select, use its value
1083            toolboxController.setLineColour(this.value);
1084        };
1085
1086        /**
1087         * Handle min/max slider change.
1088         * @param {Object} range The new range of the data.
1089         */
1090        this.onChangeMinMax = function(range) {
1091            toolboxController.setRange(range);
1092        };
1093
1094        /**
1095         * Handle undo.
1096         * @param {Object} event The associated event.
1097         */
1098        this.onUndo = function( /*event*/ ) {
1099            undoStack.undo();
1100        };
1101
1102        /**
1103         * Handle redo.
1104         * @param {Object} event The associated event.
1105         */
1106        this.onRedo = function( /*event*/ ) {
1107            undoStack.redo();
1108        };
1109
1110        /**
1111         * Handle toggle of info layer.
1112         * @param {Object} event The associated event.
1113         */
1114        this.onToggleInfoLayer = function( /*event*/ ) {
1115            self.toggleInfoLayerDisplay();
1116        };
1117
1118        /**
1119         * Handle display reset.
1120         * @param {Object} event The change event.
1121         */
1122        this.onDisplayReset = function( /*event*/ ) {
1123            self.resetLayout();
1124            self.initWLDisplay();
1125            // update preset select
1126            var select = self.getElement("presetSelect");
1127            if (select) {
1128                select.selectedIndex = 0;
1129                dwv.gui.refreshElement(select);
1130            }
1131        };
1132
1133
1134        // Private Methods -----------------------------------------------------------
1135
1136        /**
1137         * Fire an event: call all associated listeners.
1138         * @param {Object} event The event to fire.
1139         */
1140        function fireEvent(event) {
1141            if (typeof listeners[event.type] === "undefined") {
1142                return;
1143            }
1144            for (var i = 0; i < listeners[event.type].length; ++i) {
1145                listeners[event.type][i](event);
1146            }
1147        }
1148
1149        /**
1150         * Generate the image data and draw it.
1151         */
1152        function generateAndDrawImage() {
1153            // generate image data from DICOM
1154            view.generateImageData(imageData);
1155            // set the image data of the layer
1156            imageLayer.setImageData(imageData);
1157            // draw the image
1158            imageLayer.draw();
1159        }
1160
1161        /**
1162         * Apply the stored zoom to the layers.
1163         */
1164        function zoomLayers() {
1165            // image layer
1166            if (imageLayer) {
1167                imageLayer.zoom(scale, scale, scaleCenter.x, scaleCenter.y);
1168                imageLayer.draw();
1169            }
1170            // draw layer
1171            if (drawController) {
1172                drawController.zoomStage(scale, scaleCenter);
1173            }
1174            // fire event
1175            fireEvent({ "type": "zoom-change", "scale": scale, "cx": scaleCenter.x, "cy": scaleCenter.y });
1176        }
1177
1178        /**
1179         * Apply the stored translation to the layers.
1180         */
1181        function translateLayers() {
1182            // image layer
1183            if (imageLayer) {
1184                imageLayer.translate(translation.x, translation.y);
1185                imageLayer.draw();
1186                // draw layer
1187                if (drawController) {
1188                    var ox = -imageLayer.getOrigin().x / scale - translation.x;
1189                    var oy = -imageLayer.getOrigin().y / scale - translation.y;
1190                    drawController.translateStage(ox, oy);
1191                }
1192                // fire event
1193                fireEvent({
1194                    "type": "offset-change",
1195                    "scale": scale,
1196                    "cx": imageLayer.getTrans().x,
1197                    "cy": imageLayer.getTrans().y
1198                });
1199            }
1200        }
1201
1202        /**
1203         * Handle a drag over.
1204         * @private
1205         * @param {Object} event The event to handle.
1206         */
1207        function onDragOver(event) {
1208            // prevent default handling
1209            event.stopPropagation();
1210            event.preventDefault();
1211            // update box
1212            var box = self.getElement("dropBox");
1213            if (box) {
1214                box.className = 'dropBox hover';
1215            }
1216        }
1217
1218        /**
1219         * Handle a drag leave.
1220         * @private
1221         * @param {Object} event The event to handle.
1222         */
1223        function onDragLeave(event) {
1224            // prevent default handling
1225            event.stopPropagation();
1226            event.preventDefault();
1227            // update box
1228            var box = self.getElement("dropBox hover");
1229            if (box) {
1230                box.className = 'dropBox';
1231            }
1232        }
1233
1234        /**
1235         * Handle a drop event.
1236         * @private
1237         * @param {Object} event The event to handle.
1238         */
1239        function onDrop(event) {
1240            // prevent default handling
1241            event.stopPropagation();
1242            event.preventDefault();
1243            // load files
1244            self.loadFiles(event.dataTransfer.files);
1245        }
1246
1247        /**
1248         * Handle an error: display it to the user.
1249         * @private
1250         * @param {Object} error The error to handle.
1251         */
1252        function handleError(error) {
1253            // alert window
1254            if (error.name && error.message) {
1255                alert(error.name + ": " + error.message);
1256            } else {
1257                alert("Error: " + error + ".");
1258            }
1259            // log
1260            if (error.stack) {
1261                console.error(error.stack);
1262            }
1263            // stop progress
1264            dwv.gui.displayProgress(100);
1265        }
1266
1267        /**
1268         * Handle an abort: display it to the user.
1269         * @param {Object} error The error to handle.
1270         * @private
1271         */
1272        function handleAbort(error) {
1273            // log
1274            if (error.message) {
1275                console.warn(error.message);
1276            } else {
1277                console.warn("Abort called.");
1278            }
1279            // stop progress
1280            dwv.gui.displayProgress(100);
1281        }
1282
1283        /**
1284         * Handle a load progress.
1285         * @private
1286         * @param {Object} event The event to handle.
1287         */
1288        function onLoadProgress(event) {
1289            fireEvent(event);
1290            if (event.lengthComputable) {
1291                var percent = Math.ceil((event.loaded / event.total) * 100);
1292                dwv.gui.displayProgress(percent);
1293            }
1294        }
1295
1296        /**
1297         * Create the application layers.
1298         * @private
1299         * @param {Number} dataWidth The width of the input data.
1300         * @param {Number} dataHeight The height of the input data.
1301         */
1302        function createLayers(dataWidth, dataHeight) {
1303            // image layer
1304            var canImgLay = self.getElement("imageLayer");
1305            imageLayer = new dwv.html.Layer(canImgLay);
1306            imageLayer.initialise(dataWidth, dataHeight);
1307            imageLayer.fillContext();
1308            imageLayer.setStyleDisplay(true);
1309            // draw layer
1310            var drawDiv = self.getElement("drawDiv");
1311            if (drawDiv) {
1312                drawController = new dwv.DrawController(drawDiv);
1313                drawController.create(dataWidth, dataHeight);
1314            }
1315            // resize app
1316            if (fitToWindow) {
1317                self.fitToSize(dwv.gui.getWindowSize());
1318            } else {
1319                self.fitToSize({
1320                    'width': self.getElement("layerContainer").offsetWidth,
1321                    'height': self.getElement("layerContainer").offsetHeight
1322                });
1323            }
1324            self.resetLayout();
1325        }
1326
1327        /**
1328         * Post load application initialisation. To be called once the DICOM has been parsed.
1329         * @private
1330         * @param {Object} data The data to display.
1331         */
1332        function postLoadInit(data) {
1333            // only initialise the first time
1334            if (view) {
1335                return;
1336            }
1337
1338            // get the view from the loaded data
1339            view = data.view;
1340            viewController = new dwv.ViewController(view);
1341
1342            // append the DICOM tags table
1343            if (tagsGui) {
1344                tagsGui.update(data.info);
1345            }
1346            // store image
1347            originalImage = view.getImage();
1348            image = originalImage;
1349
1350            // layout
1351            var size = image.getGeometry().getSize();
1352            dataWidth = size.getNumberOfColumns();
1353            dataHeight = size.getNumberOfRows();
1354            createLayers(dataWidth, dataHeight);
1355
1356            // get the image data from the image layer
1357            imageData = imageLayer.getContext().createImageData(
1358                dataWidth, dataHeight);
1359
1360            // image listeners
1361            view.addEventListener("wl-width-change", self.onWLChange);
1362            view.addEventListener("wl-center-change", self.onWLChange);
1363            view.addEventListener("colour-change", self.onColourChange);
1364            view.addEventListener("slice-change", self.onSliceChange);
1365            view.addEventListener("frame-change", self.onFrameChange);
1366
1367            // connect with local listeners
1368            view.addEventListener("wl-width-change", fireEvent);
1369            view.addEventListener("wl-center-change", fireEvent);
1370            view.addEventListener("colour-change", fireEvent);
1371            view.addEventListener("position-change", fireEvent);
1372            view.addEventListener("slice-change", fireEvent);
1373            view.addEventListener("frame-change", fireEvent);
1374
1375            // append draw layers (before initialising the toolbox)
1376            if (drawController) {
1377                drawController.appendDrawLayer(image.getNumberOfFrames());
1378            }
1379
1380            // initialise the toolbox
1381            if (toolboxController) {
1382                toolboxController.initAndDisplay(imageLayer);
1383            }
1384
1385            // stop box listening to drag (after first drag)
1386            var box = self.getElement("dropBox");
1387            if (box) {
1388                box.removeEventListener("dragover", onDragOver);
1389                box.removeEventListener("dragleave", onDragLeave);
1390                box.removeEventListener("drop", onDrop);
1391                dwv.html.removeNode(box);
1392                // switch listening to layerContainer
1393                var div = self.getElement("layerContainer");
1394                div.addEventListener("dragover", onDragOver);
1395                div.addEventListener("dragleave", onDragLeave);
1396                div.addEventListener("drop", onDrop);
1397            }
1398
1399            // info layer
1400            var infoLayer = self.getElement("infoLayer");
1401            if (infoLayer) {
1402                infoController = new dwv.InfoController(containerDivId);
1403                infoController.create(self);
1404                infoController.toggleListeners(self, view);
1405            }
1406
1407            // init W/L display
1408            self.initWLDisplay();
1409            // generate first image
1410            generateAndDrawImage();
1411        }
1412
1413    };
1414
1415    // namespaces
1416    var dwv = dwv || {};
1417    // external
1418    var Konva = Konva || {};
1419
1420    /**
1421     * Draw controller.
1422     * @constructor
1423     * @param {Object} drawDiv The HTML div used to store the drawings.
1424     * @external Konva
1425     */
1426    dwv.DrawController = function(drawDiv) {
1427
1428        // Draw stage
1429        var drawStage = null;
1430        // Draw layers: 2 dimension array: [slice][frame]
1431        var drawLayers = [];
1432
1433        // current slice position
1434        var currentSlice = 0;
1435        // current frame position
1436        var currentFrame = 0;
1437
1438        /**
1439         * Create the controller: sets up the draw stage.
1440         * @param {Number} width The width of the stage.
1441         * @param {Number} height The height of the stage.
1442         */
1443        this.create = function(width, height) {
1444            // create stage
1445            drawStage = new Konva.Stage({
1446                'container': drawDiv,
1447                'width': width,
1448                'height': height,
1449                'listening': false
1450            });
1451            // reset style
1452            // (avoids a not needed vertical scrollbar)
1453            drawStage.getContent().setAttribute("style", "");
1454        };
1455
1456        /**
1457         * Get the current draw layer.
1458         * @return {Object} The draw layer.
1459         */
1460        this.getCurrentDrawLayer = function() {
1461            //return this.getDrawLayer(currentSlice, currentFrame);
1462            return drawLayers[currentSlice][currentFrame];
1463        };
1464
1465        /**
1466         * Reset: clear the layers array.
1467         */
1468        this.reset = function() {
1469            drawLayers = [];
1470        };
1471
1472        /**
1473         * Get the draw stage.
1474         * @return {Object} The draw layer.
1475         */
1476        this.getDrawStage = function() {
1477            return drawStage;
1478        };
1479
1480        /**
1481         * Activate the current draw layer.
1482         * @param {Object} viewController The associated view controller.
1483         */
1484        this.activateDrawLayer = function(viewController) {
1485            // hide all draw layers
1486            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1487                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1488                    drawLayers[k][f].visible(false);
1489                }
1490            }
1491            // set current position
1492            currentSlice = viewController.getCurrentPosition().k;
1493            currentFrame = viewController.getCurrentFrame();
1494            // show current draw layer
1495            var currentLayer = this.getCurrentDrawLayer();
1496            currentLayer.visible(true);
1497            currentLayer.draw();
1498        };
1499
1500        /**
1501         * Reset the stage with a new window scale.
1502         * @param {Number} windowScale The window scale.
1503         */
1504        this.resetStage = function(windowScale) {
1505            drawStage.offset({ 'x': 0, 'y': 0 });
1506            drawStage.scale({ 'x': windowScale, 'y': windowScale });
1507            drawStage.draw();
1508        };
1509
1510        /**
1511         * Resize the current stage.
1512         * @param {Number} width the stage width.
1513         * @param {Number} height the stage height.
1514         * @param {Number} scale the stage scale.
1515         */
1516        this.resizeStage = function(width, height, scale) {
1517            // resize div
1518            drawDiv.setAttribute("style", "width:" + width + "px;height:" + height + "px");
1519            // resize stage
1520            drawStage.setWidth(width);
1521            drawStage.setHeight(height);
1522            drawStage.scale({ 'x': scale, 'y': scale });
1523            drawStage.draw();
1524        };
1525
1526        /**
1527         * Zoom the stage.
1528         * @param {Number} scale The scale factor.
1529         * @param {Object} scaleCenter The scale center point.
1530         */
1531        this.zoomStage = function(scale, scaleCenter) {
1532            // zoom
1533            var newScale = { 'x': scale, 'y': scale };
1534            // offset
1535            // TODO different from the imageLayer offset?
1536            var oldScale = drawStage.scale();
1537            var oldOffset = drawStage.offset();
1538            var newOffsetX = (scaleCenter.x / oldScale.x) +
1539                oldOffset.x - (scaleCenter.x / newScale.x);
1540            var newOffsetY = (scaleCenter.y / oldScale.y) +
1541                oldOffset.y - (scaleCenter.y / newScale.y);
1542            var newOffset = { 'x': newOffsetX, 'y': newOffsetY };
1543            // store
1544            drawStage.offset(newOffset);
1545            drawStage.scale(newScale);
1546            drawStage.draw();
1547        };
1548
1549        /**
1550         * Translate the stage.
1551         * @param {Number} tx The X translation.
1552         * @param {Number} ty The Y translation.
1553         */
1554        this.translateStage = function(tx, ty) {
1555            drawStage.offset({ 'x': tx, 'y': ty });
1556            drawStage.draw();
1557        };
1558
1559        /**
1560         * Append a new draw layer list to the list.
1561         * @param {Number} nLayers The size of the layers array to append to the current one.
1562         */
1563        this.appendDrawLayer = function(nLayers) {
1564            // add a new dimension
1565            drawLayers.push([]);
1566            // fill it
1567            for (var i = 0; i < nLayers; ++i) {
1568                // create draw layer
1569                var drawLayer = new Konva.Layer({
1570                    'listening': false,
1571                    'hitGraphEnabled': false,
1572                    'visible': false
1573                });
1574                drawLayers[drawLayers.length - 1].push(drawLayer);
1575                // add the layer to the stage
1576                drawStage.add(drawLayer);
1577            }
1578        };
1579
1580        /**
1581         * Get a list of drawing display details.
1582         * @return {Object} A list of draw details including id, slice, frame...
1583         */
1584        this.getDrawDisplayDetails = function() {
1585            var list = [];
1586            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1587                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1588                    var collec = drawLayers[k][f].getChildren();
1589                    for (var i = 0, leni = collec.length; i < leni; ++i) {
1590                        var shape = collec[i].getChildren(isNodeNameShape)[0];
1591                        var label = collec[i].getChildren(isNodeNameLabel)[0];
1592                        var text = label.getChildren()[0];
1593                        var type = shape.className;
1594                        if (type === "Line") {
1595                            var shapeExtrakids = collec[i].getChildren(isNodeNameShapeExtra);
1596                            if (shape.closed()) {
1597                                type = "Roi";
1598                            } else if (shapeExtrakids.length !== 0) {
1599                                if (shapeExtrakids[0].name().indexOf("triangle") !== -1) {
1600                                    type = "Arrow";
1601                                } else {
1602                                    type = "Ruler";
1603                                }
1604                            }
1605                        }
1606                        if (type === "Rect") {
1607                            type = "Rectangle";
1608                        }
1609                        list.push({
1610                            "id": collec[i].id(),
1611                            "slice": k,
1612                            "frame": f,
1613                            "type": type,
1614                            "color": shape.stroke(),
1615                            "label": text.textExpr,
1616                            "description": text.longText
1617                        });
1618                    }
1619                }
1620            }
1621            // return
1622            return list;
1623        };
1624
1625        /**
1626         * Get all the draws of the stage.
1627         */
1628        this.getDraws = function() {
1629            var drawGroups = [];
1630            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1631                drawGroups[k] = [];
1632                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1633                    // getChildren always return, so drawings will have the good size
1634                    var groups = drawLayers[k][f].getChildren();
1635                    drawGroups[k].push(groups);
1636                }
1637            }
1638            return drawGroups;
1639        };
1640
1641        /**
1642         * Get a list of drawing store details.
1643         * @return {Object} A list of draw details including id, text, quant...
1644         * TODO Unify with getDrawDisplayDetails?
1645         */
1646        this.getDrawStoreDetails = function() {
1647            var drawingsDetails = [];
1648            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1649                drawingsDetails[k] = [];
1650                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1651                    // getChildren always return, so drawings will have the good size
1652                    var groups = drawLayers[k][f].getChildren();
1653                    var details = [];
1654                    for (var i = 0, leni = groups.length; i < leni; ++i) {
1655                        // remove anchors
1656                        var anchors = groups[i].find(".anchor");
1657                        for (var a = 0; a < anchors.length; ++a) {
1658                            anchors[a].remove();
1659                        }
1660                        // get text
1661                        var texts = groups[i].find(".text");
1662                        if (texts.length !== 1) {
1663                            console.warn("There should not be more than one text per shape.");
1664                        }
1665                        // get details (non konva vars)
1666                        details.push({
1667                            "id": groups[i].id(),
1668                            "textExpr": encodeURIComponent(texts[0].textExpr),
1669                            "longText": encodeURIComponent(texts[0].longText),
1670                            "quant": texts[0].quant
1671                        });
1672                    }
1673                    drawingsDetails[k].push(details);
1674                }
1675            }
1676            return drawingsDetails;
1677        };
1678
1679        /**
1680         * Set the drawings on the current stage.
1681         * @param {Array} drawings An array of drawings.
1682         * @param {Array} drawingsDetails An array of drawings details.
1683         * @param {Object} cmdCallback The DrawCommand callback.
1684         * @param {Object} exeCallback The callback to call once the DrawCommand has been executed.
1685         */
1686        this.setDrawings = function(drawings, drawingsDetails, cmdCallback, exeCallback) {
1687            // loop through layers
1688            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1689                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1690                    for (var i = 0, leni = drawings[k][f].length; i < leni; ++i) {
1691                        // create the group
1692                        var group = Konva.Node.create(drawings[k][f][i]);
1693                        var shape = group.getChildren(isNodeNameShape)[0];
1694                        // create the draw command
1695                        var cmd = new dwv.tool.DrawGroupCommand(
1696                            group, shape.className,
1697                            drawLayers[k][f]);
1698                        // draw command callbacks
1699                        cmd.onExecute = cmdCallback;
1700                        cmd.onUndo = cmdCallback;
1701                        // text (new in v0.2)
1702                        // TODO Verify ID?
1703                        if (drawingsDetails) {
1704                            var details = drawingsDetails[k][f][i];
1705                            var label = group.getChildren(isNodeNameLabel)[0];
1706                            var text = label.getText();
1707                            // store details
1708                            text.textExpr = details.textExpr;
1709                            text.longText = details.longText;
1710                            text.quant = details.quant;
1711                            // reset text (it was not encoded)
1712                            text.setText(dwv.utils.replaceFlags(text.textExpr, text.quant));
1713                        }
1714                        // execute
1715                        cmd.execute();
1716                        exeCallback(cmd);
1717                    }
1718                }
1719            }
1720        };
1721
1722        /**
1723         * Update a drawing from its details.
1724         * @param {Object} drawDetails Details of the drawing to update.
1725         */
1726        this.updateDraw = function(drawDetails) {
1727            // get the group
1728            var group = getDrawGroup(drawDetails.slice, drawDetails.frame, drawDetails.id);
1729            // shape
1730            var shapes = group.getChildren(isNodeNameShape);
1731            for (var i = 0; i < shapes.length; ++i) {
1732                shapes[i].stroke(drawDetails.color);
1733            }
1734            // shape extra
1735            var shapesExtra = group.getChildren(isNodeNameShapeExtra);
1736            for (var j = 0; j < shapesExtra.length; ++j) {
1737                if (typeof shapesExtra[j].stroke() !== "undefined") {
1738                    shapesExtra[j].stroke(drawDetails.color);
1739                } else if (typeof shapesExtra[j].fill() !== "undefined") {
1740                    shapesExtra[j].fill(drawDetails.color);
1741                }
1742            }
1743            // label
1744            var label = group.getChildren(isNodeNameLabel)[0];
1745            var text = label.getChildren()[0];
1746            text.fill(drawDetails.color);
1747            text.textExpr = drawDetails.label;
1748            text.longText = drawDetails.description;
1749            text.setText(dwv.utils.replaceFlags(text.textExpr, text.quant));
1750
1751            // udpate current layer
1752            this.getCurrentDrawLayer().draw();
1753        };
1754
1755        /**
1756         * Check the visibility of a given group.
1757         * @param {Object} drawDetails Details of the group to check.
1758         */
1759        this.isGroupVisible = function(drawDetails) {
1760            // get the group
1761            var group = getDrawGroup(drawDetails.slice, drawDetails.frame, drawDetails.id);
1762            // get visibility
1763            return group.isVisible();
1764        };
1765
1766        /**
1767         * Toggle the visibility of a given group.
1768         * @param {Object} drawDetails Details of the group to update.
1769         */
1770        this.toogleGroupVisibility = function(drawDetails) {
1771            // get the group
1772            var group = getDrawGroup(drawDetails.slice, drawDetails.frame, drawDetails.id);
1773            // toggle visible
1774            group.visible(!group.isVisible());
1775
1776            // udpate current layer
1777            this.getCurrentDrawLayer().draw();
1778        };
1779
1780        /**
1781         * Delete all Draws from the stage.
1782         * @param {Object} cmdCallback The DeleteCommand callback.
1783         * @param {Object} exeCallback The callback to call once the DeleteCommand has been executed.
1784         */
1785        this.deleteDraws = function(cmdCallback, exeCallback) {
1786            var delcmd, layer, groups;
1787            for (var k = 0, lenk = drawLayers.length; k < lenk; ++k) {
1788                for (var f = 0, lenf = drawLayers[k].length; f < lenf; ++f) {
1789                    layer = drawLayers[k][f];
1790                    groups = layer.getChildren();
1791                    while (groups.length) {
1792                        var shape = groups[0].getChildren(isNodeNameShape)[0];
1793                        delcmd = new dwv.tool.DeleteGroupCommand(groups[0],
1794                            dwv.tool.GetShapeDisplayName(shape), layer);
1795                        delcmd.onExecute = cmdCallback;
1796                        delcmd.onUndo = cmdCallback;
1797                        delcmd.execute();
1798                        exeCallback(delcmd);
1799                    }
1800                }
1801            }
1802        };
1803
1804        /**
1805         * Get a draw group.
1806         * @param {Number} slice The slice position.
1807         * @param {Number} frame The frame position.
1808         * @param {Number} id The group id.
1809         */
1810        function getDrawGroup(slice, frame, id) {
1811            var layer = drawLayers[slice][frame];
1812            //var collec = layer.getChildren()[drawDetails.id];
1813            var collec = layer.getChildren(function(node) {
1814                return node.id() === id;
1815            });
1816
1817            var res = null;
1818            if (collec.length !== 0) {
1819                res = collec[0];
1820            } else {
1821                console.warn("Could not find draw group for slice='" +
1822                    slice + "', frame='" + frame + "', id='" + id + "'.");
1823            }
1824            return res;
1825        }
1826
1827        /**
1828         * Is an input node's name 'shape'.
1829         * @param {Object} node A Konva node.
1830         */
1831        function isNodeNameShape(node) {
1832            return node.name() === "shape";
1833        }
1834
1835        /**
1836         * Is a node an extra shape associated with a main one.
1837         * @param {Object} node A Konva node.
1838         */
1839        function isNodeNameShapeExtra(node) {
1840            return node.name().startsWith("shape-");
1841        }
1842
1843        /**
1844         * Is an input node's name 'label'.
1845         * @param {Object} node A Konva node.
1846         */
1847        function isNodeNameLabel(node) {
1848            return node.name() === "label";
1849        }
1850
1851    }; // class dwv.DrawController
1852
1853    // namespaces
1854    var dwv = dwv || {};
1855
1856    /**
1857     * Info controller.
1858     * @constructor
1859     */
1860    dwv.InfoController = function(containerDivId) {
1861
1862        // Info layer plot gui
1863        var plotInfo = null;
1864        // Info layer colour map gui
1865        var miniColourMap = null;
1866        // Info layer overlay
1867        var overlayInfos = [];
1868        // flag to know if the info layer is listening on the image.
1869        var isInfoLayerListening = false;
1870
1871        /**
1872         * Create the different info elements.
1873         * TODO Get rid of the app input arg...
1874         */
1875        this.create = function(app) {
1876            var infocm = getElement("infocm");
1877            if (infocm) {
1878                miniColourMap = new dwv.gui.info.MiniColourMap(infocm, app);
1879                miniColourMap.create();
1880            }
1881
1882            // create overlay info at each corner
1883            var pos_list = [
1884                "tl", "tc", "tr",
1885                "cl", "cr",
1886                "bl", "bc", "br"
1887            ];
1888
1889            var num = 0;
1890            for (var n = 0; n < pos_list.length; n++) {
1891                var pos = pos_list[n];
1892                var info = getElement("info" + pos);
1893                if (info) {
1894                    overlayInfos[num] = new dwv.gui.info.Overlay(info, pos, app);
1895                    overlayInfos[num].create();
1896                    num++;
1897                }
1898            }
1899
1900            var plot = getElement("plot");
1901            if (plot) {
1902                plotInfo = new dwv.gui.info.Plot(plot, app);
1903                plotInfo.create();
1904            }
1905        };
1906
1907        /**
1908         * Toggle info listeners to the app and the view.
1909         * @param {Object} app The app to listen or not to.
1910         * @param {Object} view The view to listen or not to.
1911         */
1912        this.toggleListeners = function(app, view) {
1913            if (isInfoLayerListening) {
1914                removeListeners(app, view);
1915            } else {
1916                addListeners(app, view);
1917            }
1918        };
1919
1920        /**
1921         * Get a HTML element associated to the application.
1922         * @param name The name or id to find.
1923         * @return The found element or null.
1924         */
1925        function getElement(name) {
1926            return dwv.gui.getElement(containerDivId, name);
1927        }
1928
1929        /**
1930         * Add info listeners to the view.
1931         * @param {Object} app The app to listen to.
1932         * @param {Object} view The view to listen to.
1933         */
1934        function addListeners(app, view) {
1935            if (plotInfo) {
1936                view.addEventListener("wl-width-change", plotInfo.update);
1937                view.addEventListener("wl-center-change", plotInfo.update);
1938            }
1939            if (miniColourMap) {
1940                view.addEventListener("wl-width-change", miniColourMap.update);
1941                view.addEventListener("wl-center-change", miniColourMap.update);
1942                view.addEventListener("colour-change", miniColourMap.update);
1943            }
1944            if (overlayInfos.length > 0) {
1945                for (var n = 0; n < overlayInfos.length; n++) {
1946                    app.addEventListener("zoom-change", overlayInfos[n].update);
1947                    view.addEventListener("wl-width-change", overlayInfos[n].update);
1948                    view.addEventListener("wl-center-change", overlayInfos[n].update);
1949                    view.addEventListener("position-change", overlayInfos[n].update);
1950                    view.addEventListener("frame-change", overlayInfos[n].update);
1951                }
1952            }
1953            // udpate listening flag
1954            isInfoLayerListening = true;
1955        }
1956
1957        /**
1958         * Remove info listeners to the view.
1959         * @param {Object} app The app to stop listening to.
1960         * @param {Object} view The view to stop listening to.
1961         */
1962        function removeListeners(app, view) {
1963            if (plotInfo) {
1964                view.removeEventListener("wl-width-change", plotInfo.update);
1965                view.removeEventListener("wl-center-change", plotInfo.update);
1966            }
1967            if (miniColourMap) {
1968                view.removeEventListener("wl-width-change", miniColourMap.update);
1969                view.removeEventListener("wl-center-change", miniColourMap.update);
1970                view.removeEventListener("colour-change", miniColourMap.update);
1971            }
1972            if (overlayInfos.length > 0) {
1973                for (var n = 0; n < overlayInfos.length; n++) {
1974                    app.removeEventListener("zoom-change", overlayInfos[n].update);
1975                    view.removeEventListener("wl-width-change", overlayInfos[n].update);
1976                    view.removeEventListener("wl-center-change", overlayInfos[n].update);
1977                    view.removeEventListener("position-change", overlayInfos[n].update);
1978                    view.removeEventListener("frame-change", overlayInfos[n].update);
1979                }
1980            }
1981            // udpate listening flag
1982            isInfoLayerListening = false;
1983        }
1984
1985    }; // class dwv.InfoController
1986
1987    // namespaces
1988    var dwv = dwv || {};
1989
1990    /**
1991     * State class.
1992     * Saves: data url/path, display info.
1993     * @constructor
1994     * @param {Object} app The associated application.
1995     */
1996    dwv.State = function(app) {
1997        /**
1998         * Save the application state as JSON.
1999         */
2000        this.toJSON = function() {
2001            // store each slice drawings group
2002            var drawings = app.getDraws();
2003            var drawingsDetails = app.getDrawStoreDetails();
2004            // return a JSON string
2005            return JSON.stringify({
2006                "version": "0.2",
2007                "window-center": app.getViewController().getWindowLevel().center,
2008                "window-width": app.getViewController().getWindowLevel().width,
2009                "position": app.getViewController().getCurrentPosition(),
2010                "scale": app.getScale(),
2011                "scaleCenter": app.getScaleCenter(),
2012                "translation": app.getTranslation(),
2013                "drawings": drawings,
2014                // new in v0.2
2015                "drawingsDetails": drawingsDetails
2016            });
2017        };
2018        /**
2019         * Load an application state from JSON.
2020         * @param {String} json The JSON representation of the state.
2021         */
2022        this.fromJSON = function(json) {
2023            var data = JSON.parse(json);
2024            if (data.version === "0.1") {
2025                readV01(data);
2026            } else if (data.version === "0.2") {
2027                readV02(data);
2028            } else {
2029                throw new Error("Unknown state file format version: '" + data.version + "'.");
2030            }
2031        };
2032        /**
2033         * Read an application state from an Object in v0.1 format.
2034         * @param {Object} data The Object representation of the state.
2035         */
2036        function readV01(data) {
2037            // display
2038            app.getViewController().setWindowLevel(data["window-center"], data["window-width"]);
2039            app.getViewController().setCurrentPosition(data.position);
2040            app.zoom(data.scale, data.scaleCenter.x, data.scaleCenter.y);
2041            app.translate(data.translation.x, data.translation.y);
2042            // drawings
2043            app.setDrawings(data.drawings, null);
2044        }
2045        /**
2046         * Read an application state from an Object in v0.2 format.
2047         * @param {Object} data The Object representation of the state.
2048         */
2049        function readV02(data) {
2050            // display
2051            app.getViewController().setWindowLevel(data["window-center"], data["window-width"]);
2052            app.getViewController().setCurrentPosition(data.position);
2053            app.zoom(data.scale, data.scaleCenter.x, data.scaleCenter.y);
2054            app.translate(data.translation.x, data.translation.y);
2055            // drawings
2056            app.setDrawings(data.drawings, data.drawingsDetails);
2057        }
2058    }; // State class
2059
2060    // namespaces
2061    var dwv = dwv || {};
2062
2063    /**
2064     * Toolbox controller.
2065     * @constructor
2066     */
2067    dwv.ToolboxController = function() {
2068        // internal toolbox
2069        var toolbox = null;
2070        // point converter function
2071        var displayToIndexConverter = null;
2072
2073        /**
2074         * Create the internal toolbox.
2075         * @param {Array} toolList The list of tools instances.
2076         * @param {Object} app The associated app.
2077         */
2078        this.create = function(toolList, app) {
2079            toolbox = new dwv.tool.Toolbox(toolList, app);
2080        };
2081
2082        /**
2083         * Setup the internal toolbox.
2084         */
2085        this.setup = function() {
2086            toolbox.setup();
2087        };
2088
2089        /**
2090         * Reset the internal toolbox.
2091         */
2092        this.reset = function() {
2093            toolbox.reset();
2094        };
2095
2096        /**
2097         * Initialise and display the internal toolbox.
2098         */
2099        this.initAndDisplay = function(layer) {
2100            // initialise
2101            toolbox.init();
2102            // display
2103            toolbox.display(true);
2104            // TODO Would prefer to have this done in the addLayerListeners
2105            displayToIndexConverter = layer.displayToIndex;
2106            // add layer listeners
2107            this.addCanvasListeners(layer.getCanvas());
2108            // keydown listener
2109            window.addEventListener("keydown", onMouch, true);
2110        };
2111
2112        /**
2113         * Get the tool list.
2114         */
2115        this.getToolList = function() {
2116            return toolbox.getToolList();
2117        };
2118
2119        /**
2120         * Get the selected tool event handler.
2121         * @param {String} eventType The event type, for example mousedown, touchstart...
2122         */
2123        this.getSelectedToolEventHandler = function(eventType) {
2124            return toolbox.getSelectedTool()[eventType];
2125        };
2126
2127        /**
2128         * Set the selected tool.
2129         * @param {String} name The name of the tool.
2130         */
2131        this.setSelectedTool = function(name) {
2132            toolbox.setSelectedTool(name);
2133        };
2134
2135        /**
2136         * Set the selected shape.
2137         * @param {String} name The name of the shape.
2138         */
2139        this.setSelectedShape = function(name) {
2140            toolbox.getSelectedTool().setShapeName(name);
2141        };
2142
2143        /**
2144         * Set the selected filter.
2145         * @param {String} name The name of the filter.
2146         */
2147        this.setSelectedFilter = function(name) {
2148            toolbox.getSelectedTool().setSelectedFilter(name);
2149        };
2150
2151        /**
2152         * Run the selected filter.
2153         */
2154        this.runSelectedFilter = function() {
2155            toolbox.getSelectedTool().getSelectedFilter().run();
2156        };
2157
2158        /**
2159         * Set the tool line colour.
2160         * @param {String} colour The colour.
2161         */
2162        this.setLineColour = function(colour) {
2163            toolbox.getSelectedTool().setLineColour(colour);
2164        };
2165
2166        /**
2167         * Set the tool range.
2168         * @param {Object} range The new range of the data.
2169         */
2170        this.setRange = function(range) {
2171            // seems like jquery is checking if the method exists before it
2172            // is used...
2173            if (toolbox && toolbox.getSelectedTool() &&
2174                toolbox.getSelectedTool().getSelectedFilter()) {
2175                toolbox.getSelectedTool().getSelectedFilter().run(range);
2176            }
2177        };
2178
2179        /**
2180         * Add canvas mouse and touch listeners.
2181         * @param {Object} canvas The canvas to listen to.
2182         */
2183        this.addCanvasListeners = function(canvas) {
2184            // allow pointer events
2185            canvas.setAttribute("style", "pointer-events: auto;");
2186            // mouse listeners
2187            canvas.addEventListener("mousedown", onMouch);
2188            canvas.addEventListener("mousemove", onMouch);
2189            canvas.addEventListener("mouseup", onMouch);
2190            canvas.addEventListener("mouseout", onMouch);
2191            canvas.addEventListener("mousewheel", onMouch);
2192            canvas.addEventListener("DOMMouseScroll", onMouch);
2193            canvas.addEventListener("dblclick", onMouch);
2194            // touch listeners
2195            canvas.addEventListener("touchstart", onMouch);
2196            canvas.addEventListener("touchmove", onMouch);
2197            canvas.addEventListener("touchend", onMouch);
2198        };
2199
2200        /**
2201         * Remove canvas mouse and touch listeners.
2202         * @param {Object} canvas The canvas to stop listening to.
2203         */
2204        this.removeCanvasListeners = function(canvas) {
2205            // disable pointer events
2206            canvas.setAttribute("style", "pointer-events: none;");
2207            // mouse listeners
2208            canvas.removeEventListener("mousedown", onMouch);
2209            canvas.removeEventListener("mousemove", onMouch);
2210            canvas.removeEventListener("mouseup", onMouch);
2211            canvas.removeEventListener("mouseout", onMouch);
2212            canvas.removeEventListener("mousewheel", onMouch);
2213            canvas.removeEventListener("DOMMouseScroll", onMouch);
2214            canvas.removeEventListener("dblclick", onMouch);
2215            // touch listeners
2216            canvas.removeEventListener("touchstart", onMouch);
2217            canvas.removeEventListener("touchmove", onMouch);
2218            canvas.removeEventListener("touchend", onMouch);
2219        };
2220
2221        /**
2222         * Mou(se) and (T)ouch event handler. This function just determines the mouse/touch
2223         * position relative to the canvas element. It then passes it to the current tool.
2224         * @private
2225         * @param {Object} event The event to handle.
2226         */
2227        function onMouch(event) {
2228            // flag not to get confused between touch and mouse
2229            var handled = false;
2230            // Store the event position relative to the image canvas
2231            // in an extra member of the event:
2232            // event._x and event._y.
2233            var offsets = null;
2234            var position = null;
2235            if (event.type === "touchstart" ||
2236                event.type === "touchmove") {
2237                // event offset(s)
2238                offsets = dwv.html.getEventOffset(event);
2239                // should have at least one offset
2240                event._xs = offsets[0].x;
2241                event._ys = offsets[0].y;
2242                position = displayToIndexConverter(offsets[0]);
2243                event._x = parseInt(position.x, 10);
2244                event._y = parseInt(position.y, 10);
2245                // possible second
2246                if (offsets.length === 2) {
2247                    event._x1s = offsets[1].x;
2248                    event._y1s = offsets[1].y;
2249                    position = displayToIndexConverter(offsets[1]);
2250                    event._x1 = parseInt(position.x, 10);
2251                    event._y1 = parseInt(position.y, 10);
2252                }
2253                // set handle event flag
2254                handled = true;
2255            } else if (event.type === "mousemove" ||
2256                event.type === "mousedown" ||
2257                event.type === "mouseup" ||
2258                event.type === "mouseout" ||
2259                event.type === "mousewheel" ||
2260                event.type === "dblclick" ||
2261                event.type === "DOMMouseScroll") {
2262                offsets = dwv.html.getEventOffset(event);
2263                event._xs = offsets[0].x;
2264                event._ys = offsets[0].y;
2265                position = displayToIndexConverter(offsets[0]);
2266                event._x = parseInt(position.x, 10);
2267                event._y = parseInt(position.y, 10);
2268                // set handle event flag
2269                handled = true;
2270            } else if (event.type === "keydown" ||
2271                event.type === "touchend") {
2272                handled = true;
2273            }
2274
2275            // Call the event handler of the curently selected tool.
2276            if (handled) {
2277                if (event.type !== "keydown") {
2278                    event.preventDefault();
2279                }
2280                var func = toolbox.getSelectedTool()[event.type];
2281                if (func) {
2282                    func(event);
2283                }
2284            }
2285        }
2286
2287    }; // class dwv.ToolboxController
2288
2289    // namespaces
2290    var dwv = dwv || {};
2291
2292    /**
2293     * View controller.
2294     * @constructor
2295     */
2296    dwv.ViewController = function(view) {
2297        // closure to self
2298        var self = this;
2299        // Slice/frame player ID (created by setInterval)
2300        var playerID = null;
2301
2302        /**
2303         * Get the window/level presets names.
2304         * @return {Array} The presets names.
2305         */
2306        this.getWindowLevelPresetsNames = function() {
2307            return view.getWindowPresetsNames();
2308        };
2309
2310        /**
2311         * Add window/level presets to the view.
2312         * @return {Object} The list of presets.
2313         */
2314        this.addWindowLevelPresets = function(presets) {
2315            return view.addWindowPresets(presets);
2316        };
2317
2318        /**
2319         * Set the window level to the preset with the input name.
2320         * @param {String} name The name of the preset to activate.
2321         */
2322        this.setWindowLevelPreset = function(name) {
2323            view.setWindowLevelPreset(name);
2324        };
2325
2326        /**
2327         * Set the window level to the preset with the input id.
2328         * @param {Number} id The id of the preset to activate.
2329         */
2330        this.setWindowLevelPresetById = function(id) {
2331            view.setWindowLevelPresetById(id);
2332        };
2333
2334        /**
2335         * Check if the controller is playing.
2336         * @return {Boolean} True is the controler is playing slices/frames.
2337         */
2338        this.isPlaying = function() { return (playerID !== null); };
2339
2340        /**
2341         * Get the current position.
2342         * @return {Object} The position.
2343         */
2344        this.getCurrentPosition = function() {
2345            return view.getCurrentPosition();
2346        };
2347
2348        /**
2349         * Set the current position.
2350         * @param {Object} pos The position.
2351         * @return {Boolean} False if not in bounds.
2352         */
2353        this.setCurrentPosition = function(pos) {
2354            return view.setCurrentPosition(pos);
2355        };
2356
2357        /**
2358         * Set the current 2D (i,j) position.
2359         * @param {Number} i The column index.
2360         * @param {Number} j The row index.
2361         * @return {Boolean} False if not in bounds.
2362         */
2363        this.setCurrentPosition2D = function(i, j) {
2364            return view.setCurrentPosition({
2365                "i": i,
2366                "j": j,
2367                "k": view.getCurrentPosition().k
2368            });
2369        };
2370
2371        /**
2372         * Set the current slice position.
2373         * @param {Number} k The slice index.
2374         * @return {Boolean} False if not in bounds.
2375         */
2376        this.setCurrentSlice = function(k) {
2377            return view.setCurrentPosition({
2378                "i": view.getCurrentPosition().i,
2379                "j": view.getCurrentPosition().j,
2380                "k": k
2381            });
2382        };
2383
2384        /**
2385         * Increment the current slice number.
2386         * @return {Boolean} False if not in bounds.
2387         */
2388        this.incrementSliceNb = function() {
2389            return self.setCurrentSlice(view.getCurrentPosition().k + 1);
2390        };
2391
2392        /**
2393         * Decrement the current slice number.
2394         * @return {Boolean} False if not in bounds.
2395         */
2396        this.decrementSliceNb = function() {
2397            return self.setCurrentSlice(view.getCurrentPosition().k - 1);
2398        };
2399
2400        /**
2401         * Get the current frame.
2402         * @return {Number} The frame number.
2403         */
2404        this.getCurrentFrame = function() {
2405            return view.getCurrentFrame();
2406        };
2407
2408        /**
2409         * Set the current frame.
2410         * @param {Number} number The frame number.
2411         * @return {Boolean} False if not in bounds.
2412         */
2413        this.setCurrentFrame = function(number) {
2414            return view.setCurrentFrame(number);
2415        };
2416
2417        /**
2418         * Increment the current frame.
2419         * @return {Boolean} False if not in bounds.
2420         */
2421        this.incrementFrameNb = function() {
2422            return view.setCurrentFrame(view.getCurrentFrame() + 1);
2423        };
2424
2425        /**
2426         * Decrement the current frame.
2427         * @return {Boolean} False if not in bounds.
2428         */
2429        this.decrementFrameNb = function() {
2430            return view.setCurrentFrame(view.getCurrentFrame() - 1);
2431        };
2432
2433        /**
2434         * Go to first slice .
2435         * @return {Boolean} False if not in bounds.
2436         * @deprecated Use the setCurrentSlice function.
2437         */
2438        this.goFirstSlice = function() {
2439            return view.setCurrentPosition({
2440                "i": view.getCurrentPosition().i,
2441                "j": view.getCurrentPosition().j,
2442                "k": 0
2443            });
2444        };
2445
2446        /**
2447         *
2448         */
2449        this.play = function() {
2450            if (playerID === null) {
2451                var nSlices = view.getImage().getGeometry().getSize().getNumberOfSlices();
2452                var nFrames = view.getImage().getNumberOfFrames();
2453
2454                playerID = setInterval(function() {
2455                    if (nSlices !== 1) {
2456                        if (!self.incrementSliceNb()) {
2457                            self.setCurrentSlice(0);
2458                        }
2459                    } else if (nFrames !== 1) {
2460                        if (!self.incrementFrameNb()) {
2461                            self.setCurrentFrame(0);
2462                        }
2463                    }
2464
2465                }, 300);
2466            } else {
2467                this.stop();
2468            }
2469        };
2470
2471        /**
2472         *
2473         */
2474        this.stop = function() {
2475            if (playerID !== null) {
2476                clearInterval(playerID);
2477                playerID = null;
2478            }
2479        };
2480
2481        /**
2482         * Get the window/level.
2483         * @return {Object} The window center and width.
2484         */
2485        this.getWindowLevel = function() {
2486            return {
2487                "width": view.getCurrentWindowLut().getWindowLevel().getWidth(),
2488                "center": view.getCurrentWindowLut().getWindowLevel().getCenter()
2489            };
2490        };
2491
2492        /**
2493         * Set the window/level.
2494         * @param {Number} wc The window center.
2495         * @param {Number} ww The window width.
2496         */
2497        this.setWindowLevel = function(wc, ww) {
2498            view.setWindowLevel(wc, ww);
2499        };
2500
2501        /**
2502         * Get the colour map.
2503         * @return {Object} The colour map.
2504         */
2505        this.getColourMap = function() {
2506            return view.getColourMap();
2507        };
2508
2509        /**
2510         * Set the colour map.
2511         * @param {Object} colourMap The colour map.
2512         */
2513        this.setColourMap = function(colourMap) {
2514            view.setColourMap(colourMap);
2515        };
2516
2517        /**
2518         * Set the colour map from a name.
2519         * @param {String} name The name of the colour map to set.
2520         */
2521        this.setColourMapFromName = function(name) {
2522            // check if we have it
2523            if (!dwv.tool.colourMaps[name]) {
2524                throw new Error("Unknown colour map: '" + name + "'");
2525            }
2526            // enable it
2527            this.setColourMap(dwv.tool.colourMaps[name]);
2528        };
2529
2530    }; // class dwv.ViewController
2531
2532    // namespaces
2533    var dwv = dwv || {};
2534    /** @namespace */
2535    dwv.dicom = dwv.dicom || {};
2536
2537    /**
2538     * Get the version of the library.
2539     * @return {String} The version of the library.
2540     */
2541    dwv.getVersion = function() { return "0.22.1"; };
2542
2543    /**
2544     * Clean string: trim and remove ending.
2545     * @param {String} inputStr The string to clean.
2546     * @return {String} The cleaned string.
2547     */
2548    dwv.dicom.cleanString = function(inputStr) {
2549        var res = inputStr;
2550        if (inputStr) {
2551            // trim spaces
2552            res = inputStr.trim();
2553            // get rid of ending zero-width space (u200B)
2554            if (res[res.length - 1] === String.fromCharCode("u200B")) {
2555                res = res.substring(0, res.length - 1);
2556            }
2557        }
2558        return res;
2559    };
2560
2561    /**
2562     * Is the Native endianness Little Endian.
2563     * @type Boolean
2564     */
2565    dwv.dicom.isNativeLittleEndian = function() {
2566        return new Int8Array(new Int16Array([1]).buffer)[0] > 0;
2567    };
2568
2569    /**
2570     * Get the utfLabel (used by the TextDecoder) from a character set term
2571     * References:
2572     * - DICOM [Value Encoding]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_6.html}
2573     * - DICOM [Specific Character Set]{@link http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2}
2574     * - [TextDecoder#Parameters]{@link https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder#Parameters}
2575     */
2576    dwv.dicom.getUtfLabel = function(charSetTerm) {
2577        var label = "utf-8";
2578        if (charSetTerm === "ISO_IR 100") {
2579            label = "iso-8859-1";
2580        } else if (charSetTerm === "ISO_IR 101") {
2581            label = "iso-8859-2";
2582        } else if (charSetTerm === "ISO_IR 109") {
2583            label = "iso-8859-3";
2584        } else if (charSetTerm === "ISO_IR 110") {
2585            label = "iso-8859-4";
2586        } else if (charSetTerm === "ISO_IR 144") {
2587            label = "iso-8859-5";
2588        } else if (charSetTerm === "ISO_IR 127") {
2589            label = "iso-8859-6";
2590        } else if (charSetTerm === "ISO_IR 126") {
2591            label = "iso-8859-7";
2592        } else if (charSetTerm === "ISO_IR 138") {
2593            label = "iso-8859-8";
2594        } else if (charSetTerm === "ISO_IR 148") {
2595            label = "iso-8859-9";
2596        } else if (charSetTerm === "ISO_IR 13") {
2597            label = "shift-jis";
2598        } else if (charSetTerm === "ISO_IR 166") {
2599            label = "iso-8859-11";
2600        } else if (charSetTerm === "ISO 2022 IR 87") {
2601            label = "iso-2022-jp";
2602        } else if (charSetTerm === "ISO 2022 IR 149") {
2603            // not supported by TextDecoder when it says it should...
2604            //label = "iso-2022-kr";
2605        } else if (charSetTerm === "ISO 2022 IR 58") {
2606            // not supported by TextDecoder...
2607            //label = "iso-2022-cn";
2608        } else if (charSetTerm === "ISO_IR 192") {
2609            label = "utf-8";
2610        } else if (charSetTerm === "GB18030") {
2611            label = "gb18030";
2612        } else if (charSetTerm === "GB2312") {
2613            label = "gb2312";
2614        } else if (charSetTerm === "GBK") {
2615            label = "chinese";
2616        }
2617        return label;
2618    };
2619
2620    /**
2621     * Data reader.
2622     * @constructor
2623     * @param {Array} buffer The input array buffer.
2624     * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian.
2625     */
2626    dwv.dicom.DataReader = function(buffer, isLittleEndian) {
2627        // Set endian flag if not defined.
2628        if (typeof isLittleEndian === 'undefined') {
2629            isLittleEndian = true;
2630        }
2631
2632        // Default text decoder
2633        var defaultTextDecoder = {};
2634        defaultTextDecoder.decode = function(buffer) {
2635            var result = "";
2636            for (var i = 0, leni = buffer.length; i < leni; ++i) {
2637                result += String.fromCharCode(buffer[i]);
2638            }
2639            return result;
2640        };
2641        // Text decoder
2642        var textDecoder = defaultTextDecoder;
2643        if (typeof window.TextDecoder !== "undefined") {
2644            textDecoder = new TextDecoder("iso-8859-1");
2645        }
2646
2647        /**
2648         * Set the utfLabel used to construct the TextDecoder.
2649         * @param {String} label The encoding label.
2650         */
2651        this.setUtfLabel = function(label) {
2652            if (typeof window.TextDecoder !== "undefined") {
2653                textDecoder = new TextDecoder(label);
2654            }
2655        };
2656
2657        /**
2658         * Is the Native endianness Little Endian.
2659         * @private
2660         * @type Boolean
2661         */
2662        var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian();
2663
2664        /**
2665         * Flag to know if the TypedArray data needs flipping.
2666         * @private
2667         * @type Boolean
2668         */
2669        var needFlip = (isLittleEndian !== isNativeLittleEndian);
2670
2671        /**
2672         * The main data view.
2673         * @private
2674         * @type DataView
2675         */
2676        var view = new DataView(buffer);
2677
2678        /**
2679         * Flip an array's endianness.
2680         * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}.
2681         * @param {Object} array The array to flip (modified).
2682         */
2683        this.flipArrayEndianness = function(array) {
2684            var blen = array.byteLength;
2685            var u8 = new Uint8Array(array.buffer, array.byteOffset, blen);
2686            var bpel = array.BYTES_PER_ELEMENT;
2687            var tmp;
2688            for (var i = 0; i < blen; i += bpel) {
2689                for (var j = i + bpel - 1, k = i; j > k; j--, k++) {
2690                    tmp = u8[k];
2691                    u8[k] = u8[j];
2692                    u8[j] = tmp;
2693                }
2694            }
2695        };
2696
2697        /**
2698         * Read Uint16 (2 bytes) data.
2699         * @param {Number} byteOffset The offset to start reading from.
2700         * @return {Number} The read data.
2701         */
2702        this.readUint16 = function(byteOffset) {
2703            return view.getUint16(byteOffset, isLittleEndian);
2704        };
2705        /**
2706         * Read Uint32 (4 bytes) data.
2707         * @param {Number} byteOffset The offset to start reading from.
2708         * @return {Number} The read data.
2709         */
2710        this.readUint32 = function(byteOffset) {
2711            return view.getUint32(byteOffset, isLittleEndian);
2712        };
2713        /**
2714         * Read Int32 (4 bytes) data.
2715         * @param {Number} byteOffset The offset to start reading from.
2716         * @return {Number} The read data.
2717         */
2718        this.readInt32 = function(byteOffset) {
2719            return view.getInt32(byteOffset, isLittleEndian);
2720        };
2721        /**
2722         * Read Uint8 array.
2723         * @param {Number} byteOffset The offset to start reading from.
2724         * @param {Number} size The size of the array.
2725         * @return {Array} The read data.
2726         */
2727        this.readUint8Array = function(byteOffset, size) {
2728            return new Uint8Array(buffer, byteOffset, size);
2729        };
2730        /**
2731         * Read Int8 array.
2732         * @param {Number} byteOffset The offset to start reading from.
2733         * @param {Number} size The size of the array.
2734         * @return {Array} The read data.
2735         */
2736        this.readInt8Array = function(byteOffset, size) {
2737            return new Int8Array(buffer, byteOffset, size);
2738        };
2739        /**
2740         * Read Uint16 array.
2741         * @param {Number} byteOffset The offset to start reading from.
2742         * @param {Number} size The size of the array.
2743         * @return {Array} The read data.
2744         */
2745        this.readUint16Array = function(byteOffset, size) {
2746            var arraySize = size / Uint16Array.BYTES_PER_ELEMENT;
2747            var data = null;
2748            // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2)
2749            if ((byteOffset % Uint16Array.BYTES_PER_ELEMENT) === 0) {
2750                data = new Uint16Array(buffer, byteOffset, arraySize);
2751                if (needFlip) {
2752                    this.flipArrayEndianness(data);
2753                }
2754            } else {
2755                data = new Uint16Array(arraySize);
2756                for (var i = 0; i < arraySize;
2756 ++i) {
2757                    data[i] = view.getInt16((byteOffset +
2758                            Uint16Array.BYTES_PER_ELEMENT * i),
2759                        isLittleEndian);
2760                }
2761            }
2762            return data;
2763        };
2764        /**
2765         * Read Int16 array.
2766         * @param {Number} byteOffset The offset to start reading from.
2767         * @param {Number} size The size of the array.
2768         * @return {Array} The read data.
2769         */
2770        this.readInt16Array = function(byteOffset, size) {
2771            var arraySize = size / Int16Array.BYTES_PER_ELEMENT;
2772            var data = null;
2773            // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2)
2774            if ((byteOffset % Int16Array.BYTES_PER_ELEMENT) === 0) {
2775                data = new Int16Array(buffer, byteOffset, arraySize);
2776                if (needFlip) {
2777                    this.flipArrayEndianness(data);
2778                }
2779            } else {
2780                data = new Int16Array(arraySize);
2781                for (var i = 0; i < arraySize; ++i) {
2782                    data[i] = view.getInt16((byteOffset +
2783                            Int16Array.BYTES_PER_ELEMENT * i),
2784                        isLittleEndian);
2785                }
2786            }
2787            return data;
2788        };
2789        /**
2790         * Read Uint32 array.
2791         * @param {Number} byteOffset The offset to start reading from.
2792         * @param {Number} size The size of the array.
2793         * @return {Array} The read data.
2794         */
2795        this.readUint32Array = function(byteOffset, size) {
2796            var arraySize = size / Uint32Array.BYTES_PER_ELEMENT;
2797            var data = null;
2798            // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4)
2799            if ((byteOffset % Uint32Array.BYTES_PER_ELEMENT) === 0) {
2800                data = new Uint32Array(buffer, byteOffset, arraySize);
2801                if (needFlip) {
2802                    this.flipArrayEndianness(data);
2803                }
2804            } else {
2805                data = new Uint32Array(arraySize);
2806                for (var i = 0; i < arraySize; ++i) {
2807                    data[i] = view.getUint32((byteOffset +
2808                            Uint32Array.BYTES_PER_ELEMENT * i),
2809                        isLittleEndian);
2810                }
2811            }
2812            return data;
2813        };
2814        /**
2815         * Read Int32 array.
2816         * @param {Number} byteOffset The offset to start reading from.
2817         * @param {Number} size The size of the array.
2818         * @return {Array} The read data.
2819         */
2820        this.readInt32Array = function(byteOffset, size) {
2821            var arraySize = size / Int32Array.BYTES_PER_ELEMENT;
2822            var data = null;
2823            // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4)
2824            if ((byteOffset % Int32Array.BYTES_PER_ELEMENT) === 0) {
2825                data = new Int32Array(buffer, byteOffset, arraySize);
2826                if (needFlip) {
2827                    this.flipArrayEndianness(data);
2828                }
2829            } else {
2830                data = new Int32Array(arraySize);
2831                for (var i = 0; i < arraySize; ++i) {
2832                    data[i] = view.getInt32((byteOffset +
2833                            Int32Array.BYTES_PER_ELEMENT * i),
2834                        isLittleEndian);
2835                }
2836            }
2837            return data;
2838        };
2839        /**
2840         * Read Float32 array.
2841         * @param {Number} byteOffset The offset to start reading from.
2842         * @param {Number} size The size of the array.
2843         * @return {Array} The read data.
2844         */
2845        this.readFloat32Array = function(byteOffset, size) {
2846            var arraySize = size / Float32Array.BYTES_PER_ELEMENT;
2847            var data = null;
2848            // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4)
2849            if ((byteOffset % Float32Array.BYTES_PER_ELEMENT) === 0) {
2850                data = new Float32Array(buffer, byteOffset, arraySize);
2851                if (needFlip) {
2852                    this.flipArrayEndianness(data);
2853                }
2854            } else {
2855                data = new Float32Array(arraySize);
2856                for (var i = 0; i < arraySize;
2856 ++i) {
2857                    data[i] = view.getFloat32((byteOffset +
2858                            Float32Array.BYTES_PER_ELEMENT * i),
2859                        isLittleEndian);
2860                }
2861            }
2862            return data;
2863        };
2864        /**
2865         * Read Float64 array.
2866         * @param {Number} byteOffset The offset to start reading from.
2867         * @param {Number} size The size of the array.
2868         * @return {Array} The read data.
2869         */
2870        this.readFloat64Array = function(byteOffset, size) {
2871            var arraySize = size / Float64Array.BYTES_PER_ELEMENT;
2872            var data = null;
2873            // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8)
2874            if ((byteOffset % Float64Array.BYTES_PER_ELEMENT) === 0) {
2875                data = new Float64Array(buffer, byteOffset, arraySize);
2876                if (needFlip) {
2877                    this.flipArrayEndianness(data);
2878                }
2879            } else {
2880                data = new Float64Array(arraySize);
2881                for (var i = 0; i < arraySize; ++i) {
2882                    data[i] = view.getFloat64((byteOffset +
2883                            Float64Array.BYTES_PER_ELEMENT * i),
2884                        isLittleEndian);
2885                }
2886            }
2887            return data;
2888        };
2889        /**
2890         * Read data as an hexadecimal string.
2891         * @param {Number} byteOffset The offset to start reading from.
2892         * @return {Array} The read data.
2893         */
2894        this.readHex = function(byteOffset) {
2895            // read and convert to hex string
2896            var str = this.readUint16(byteOffset).toString(16);
2897            // return padded
2898            return "0x0000".substr(0, 6 - str.length) + str.toUpperCase();
2899        };
2900
2901        /**
2902         * Read data as a string.
2903         * @param {Number} byteOffset The offset to start reading from.
2904         * @param {Number} nChars The number of characters to read.
2905         * @return {String} The read data.
2906         */
2907        this.readString = function(byteOffset, nChars) {
2908            var data = this.readUint8Array(byteOffset, nChars);
2909            return defaultTextDecoder.decode(data);
2910        };
2911
2912        /**
2913         * Read data as a 'special' string, decoding it if the TextDecoder is available.
2914         * @param {Number} byteOffset The offset to start reading from.
2915         * @param {Number} nChars The number of characters to read.
2916         * @return {String} The read data.
2917         */
2918        this.readSpecialString = function(byteOffset, nChars) {
2919            var data = this.readUint8Array(byteOffset, nChars);
2920            return textDecoder.decode(data);
2921        };
2922
2923    };
2924
2925    /**
2926     * Get the group-element pair from a tag string name.
2927     * @param {String} tagName The tag string name.
2928     * @return {Object} group-element pair.
2929     */
2930    dwv.dicom.getGroupElementFromName = function(tagName) {
2931        var group = null;
2932        var element = null;
2933        var dict = dwv.dicom.dictionary;
2934        var keys0 = Object.keys(dict);
2935        var keys1 = null;
2936        // label for nested loop break
2937        outLabel:
2938            // search through dictionary
2939            for (var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) {
2940                group = keys0[k0];
2941                keys1 = Object.keys(dict[group]);
2942                for (var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) {
2943                    element = keys1[k1];
2944                    if (dict[group][element][2] === tagName) {
2945                        break outLabel;
2946                    }
2947                }
2948            }
2949        return { 'group': group, 'element': element };
2950    };
2951
2952    /**
2953     * Immutable tag.
2954     * @constructor
2955     * @param {String} group The tag group.
2956     * @param {String} element The tag element.
2957     */
2958    dwv.dicom.Tag = function(group, element) {
2959        /**
2960         * Get the tag group.
2961         * @return {String} The tag group.
2962         */
2963        this.getGroup = function() { return group; };
2964        /**
2965         * Get the tag element.
2966         * @return {String} The tag element.
2967         */
2968        this.getElement = function() { return element; };
2969    }; // Tag class
2970
2971    /**
2972     * Check for Tag equality.
2973     * @param {Object} rhs The other tag to compare to.
2974     * @return {Boolean} True if both tags are equal.
2975     */
2976    dwv.dicom.Tag.prototype.equals = function(rhs) {
2977        return rhs !== null &&
2978            this.getGroup() === rhs.getGroup() &&
2979            this.getElement() === rhs.getElement();
2980    };
2981
2982    /**
2983     * Check for Tag equality.
2984     * @param {Object} rhs The other tag to compare to provided as a simple object.
2985     * @return {Boolean} True if both tags are equal.
2986     */
2987    dwv.dicom.Tag.prototype.equals2 = function(rhs) {
2988        if (rhs === null ||
2989            typeof rhs.group === "undefined" ||
2990            typeof rhs.element === "undefined") {
2991            return false;
2992        }
2993        return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element));
2994    };
2995
2996    // Get the FileMetaInformationGroupLength Tag.
2997    dwv.dicom.getFileMetaInformationGroupLengthTag = function() {
2998        return new dwv.dicom.Tag("0x0002", "0x0000");
2999    };
3000    // Get the Item Tag.
3001    dwv.dicom.getItemTag = function() {
3002        return new dwv.dicom.Tag("0xFFFE", "0xE000");
3003    };
3004    // Get the ItemDelimitationItem Tag.
3005    dwv.dicom.getItemDelimitationItemTag = function() {
3006        return new dwv.dicom.Tag("0xFFFE", "0xE00D");
3007    };
3008    // Get the SequenceDelimitationItem Tag.
3009    dwv.dicom.getSequenceDelimitationItemTag = function() {
3010        return new dwv.dicom.Tag("0xFFFE", "0xE0DD");
3011    };
3012    // Get the PixelData Tag.
3013    dwv.dicom.getPixelDataTag = function() {
3014        return new dwv.dicom.Tag("0x7FE0", "0x0010");
3015    };
3016
3017    /**
3018     * Get the group-element key used to store DICOM elements.
3019     * @param {Number} group The DICOM group.
3020     * @param {Number} element The DICOM element.
3021     * @return {String} The key.
3022     */
3023    dwv.dicom.getGroupElementKey = function(group, element) {
3024        return 'x' + group.substr(2, 6) + element.substr(2, 6);
3025    };
3026
3027    /**
3028     * Split a group-element key used to store DICOM elements.
3029     * @param {String} key The key in form "x00280102.
3030     * @return {Object} The DICOM group and element.
3031     */
3032    dwv.dicom.splitGroupElementKey = function(key) {
3033        return { 'group': key.substr(1, 4), 'element': key.substr(5, 8) };
3034    };
3035
3036    /**
3037     * Get patient orientation label in the reverse direction.
3038     * @param {String} ori Patient Orientation value.
3039     * @return {String} Reverse Orientation Label.
3040     */
3041    dwv.dicom.getReverseOrientation = function(ori) {
3042        if (!ori) {
3043            return null;
3044        }
3045        // reverse labels
3046        var rlabels = {
3047            "L": "R",
3048            "R": "L",
3049            "A": "P",
3050            "P": "A",
3051            "H": "F",
3052            "F": "H"
3053        };
3054
3055        var rori = "";
3056        for (var n = 0; n < ori.length; n++) {
3057            var o = ori.substr(n, 1);
3058            var r = rlabels[o];
3059            if (r) {
3060                rori += r;
3061            }
3062        }
3063        // return
3064        return rori;
3065    };
3066
3067    /**
3068     * Tell if a given syntax is an implicit one (element with no VR).
3069     * @param {String} syntax The transfer syntax to test.
3070     * @return {Boolean} True if an implicit syntax.
3071     */
3072    dwv.dicom.isImplicitTransferSyntax = function(syntax) {
3073        return syntax === "1.2.840.10008.1.2";
3074    };
3075
3076    /**
3077     * Tell if a given syntax is a big endian syntax.
3078     * @param {String} syntax The transfer syntax to test.
3079     * @return {Boolean} True if a big endian syntax.
3080     */
3081    dwv.dicom.isBigEndianTransferSyntax = function(syntax) {
3082        return syntax === "1.2.840.10008.1.2.2";
3083    };
3084
3085    /**
3086     * Tell if a given syntax is a JPEG baseline one.
3087     * @param {String} syntax The transfer syntax to test.
3088     * @return {Boolean} True if a jpeg baseline syntax.
3089     */
3090    dwv.dicom.isJpegBaselineTransferSyntax = function(syntax) {
3091        return syntax === "1.2.840.10008.1.2.4.50" ||
3092            syntax === "1.2.840.10008.1.2.4.51";
3093    };
3094
3095    /**
3096     * Tell if a given syntax is a retired JPEG one.
3097     * @param {String} syntax The transfer syntax to test.
3098     * @return {Boolean} True if a retired jpeg syntax.
3099     */
3100    dwv.dicom.isJpegRetiredTransferSyntax = function(syntax) {
3101        return (syntax.match(/1.2.840.10008.1.2.4.5/) !== null &&
3102                !dwv.dicom.isJpegBaselineTransferSyntax() &&
3103                !dwv.dicom.isJpegLosslessTransferSyntax()) ||
3104            syntax.match(/1.2.840.10008.1.2.4.6/) !== null;
3105    };
3106
3107    /**
3108     * Tell if a given syntax is a JPEG Lossless one.
3109     * @param {String} syntax The transfer syntax to test.
3110     * @return {Boolean} True if a jpeg lossless syntax.
3111     */
3112    dwv.dicom.isJpegLosslessTransferSyntax = function(syntax) {
3113        return syntax === "1.2.840.10008.1.2.4.57" ||
3114            syntax === "1.2.840.10008.1.2.4.70";
3115    };
3116
3117    /**
3118     * Tell if a given syntax is a JPEG-LS one.
3119     * @param {String} syntax The transfer syntax to test.
3120     * @return {Boolean} True if a jpeg-ls syntax.
3121     */
3122    dwv.dicom.isJpeglsTransferSyntax = function(syntax) {
3123        return syntax.match(/1.2.840.10008.1.2.4.8/) !== null;
3124    };
3125
3126    /**
3127     * Tell if a given syntax is a JPEG 2000 one.
3128     * @param {String} syntax The transfer syntax to test.
3129     * @return {Boolean} True if a jpeg 2000 syntax.
3130     */
3131    dwv.dicom.isJpeg2000TransferSyntax = function(syntax) {
3132        return syntax.match(/1.2.840.10008.1.2.4.9/) !== null;
3133    };
3134
3135    /**
3136     * Tell if a given syntax needs decompression.
3137     * @param {String} syntax The transfer syntax to test.
3138     * @return {String} The name of the decompression algorithm.
3139     */
3140    dwv.dicom.getSyntaxDecompressionName = function(syntax) {
3141        var algo = null;
3142        if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) {
3143            algo = "jpeg2000";
3144        } else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) {
3145            algo = "jpeg-baseline";
3146        } else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) {
3147            algo = "jpeg-lossless";
3148        }
3149        return algo;
3150    };
3151
3152    /**
3153     * Tell if a given syntax is supported for reading.
3154     * @param {String} syntax The transfer syntax to test.
3155     * @return {Boolean} True if a supported syntax.
3156     */
3157    dwv.dicom.isReadSupportedTransferSyntax = function(syntax) {
3158
3159        // Unsupported:
3160        // "1.2.840.10008.1.2.1.99": Deflated Explicit VR - Little Endian
3161        // "1.2.840.10008.1.2.4.100": MPEG2 Image Compression
3162        // dwv.dicom.isJpegRetiredTransferSyntax(syntax): non supported JPEG
3163        // dwv.dicom.isJpeglsTransferSyntax(syntax): JPEG-LS
3164        // "1.2.840.10008.1.2.5": RLE (lossless)
3165
3166        return (syntax === "1.2.840.10008.1.2" || // Implicit VR - Little Endian
3167            syntax === "1.2.840.10008.1.2.1" || // Explicit VR - Little Endian
3168            syntax === "1.2.840.10008.1.2.2" || // Explicit VR - Big Endian
3169            dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline
3170            dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless
3171            dwv.dicom.isJpeg2000TransferSyntax(syntax)); // JPEG 2000
3172    };
3173
3174    /**
3175     * Get the transfer syntax name.
3176     * Reference: [UID Values]{@link http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html}.
3177     * @param {String} syntax The transfer syntax.
3178     * @return {String} The name of the transfer syntax.
3179     */
3180    dwv.dicom.getTransferSyntaxName = function(syntax) {
3181        var name = "Unknown";
3182        // Implicit VR - Little Endian
3183        if (syntax === "1.2.840.10008.1.2") {
3184            name = "Little Endian Implicit";
3185        }
3186        // Explicit VR - Little Endian
3187        else if (syntax === "1.2.840.10008.1.2.1") {
3188            name = "Little Endian Explicit";
3189        }
3190        // Deflated Explicit VR - Little Endian
3191        else if (syntax === "1.2.840.10008.1.2.1.99") {
3192            name = "Little Endian Deflated Explicit";
3193        }
3194        // Explicit VR - Big Endian
3195        else if (syntax === "1.2.840.10008.1.2.2") {
3196            name = "Big Endian Explicit";
3197        }
3198        // JPEG baseline
3199        else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) {
3200            if (syntax === "1.2.840.10008.1.2.4.50") {
3201                name = "JPEG Baseline";
3202            } else { // *.51
3203                name = "JPEG Extended, Process 2+4";
3204            }
3205        }
3206        // JPEG Lossless
3207        else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) {
3208            if (syntax === "1.2.840.10008.1.2.4.57") {
3209                name = "JPEG Lossless, Nonhierarchical (Processes 14)";
3210            } else { // *.70
3211                name = "JPEG Lossless, Non-hierarchical, 1st Order Prediction";
3212            }
3213        }
3214        // Retired JPEG
3215        else if (dwv.dicom.isJpegRetiredTransferSyntax(syntax)) {
3216            name = "Retired JPEG";
3217        }
3218        // JPEG-LS
3219        else if (dwv.dicom.isJpeglsTransferSyntax(syntax)) {
3220            name = "JPEG-LS";
3221        }
3222        // JPEG 2000
3223        else if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) {
3224            if (syntax === "1.2.840.10008.1.2.4.91") {
3225                name = "JPEG 2000 (Lossless or Lossy)";
3226            } else { // *.90
3227                name = "JPEG 2000 (Lossless only)";
3228            }
3229        }
3230        // MPEG2 Image Compression
3231        else if (syntax === "1.2.840.10008.1.2.4.100") {
3232            name = "MPEG2";
3233        }
3234        // RLE (lossless)
3235        else if (syntax === "1.2.840.10008.1.2.5") {
3236            name = "RLE";
3237        }
3238        // return
3239        return name;
3240    };
3241
3242    /**
3243     * Get the appropriate TypedArray in function of arguments.
3244     * @param {Number} bitsAllocated The number of bites used to store the data: [8, 16, 32].
3245     * @param {Number} pixelRepresentation The pixel representation, 0:unsigned;1:signed.
3246     * @param {Size} size The size of the new array.
3247     * @return The good typed array.
3248     */
3249    dwv.dicom.getTypedArray = function(bitsAllocated, pixelRepresentation, size) {
3250        var res = null;
3251        if (bitsAllocated === 8) {
3252            if (pixelRepresentation === 0) {
3253                res = new Uint8Array(size);
3254            } else {
3255                res = new Int8Array(size);
3256            }
3257        } else if (bitsAllocated === 16) {
3258            if (pixelRepresentation === 0) {
3259                res = new Uint16Array(size);
3260            } else {
3261                res = new Int16Array(size);
3262            }
3263        } else if (bitsAllocated === 32) {
3264            if (pixelRepresentation === 0) {
3265                res = new Uint32Array(size);
3266            } else {
3267                res = new Int32Array(size);
3268            }
3269        }
3270        return res;
3271    };
3272
3273    /**
3274     * Does this Value Representation (VR) have a 32bit Value Length (VL).
3275     * Ref: [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}.
3276     * @param {String} vr The data Value Representation (VR).
3277     * @returns {Boolean} True if this VR has a 32-bit VL.
3278     */
3279    dwv.dicom.is32bitVLVR = function(vr) {
3280        // added locally used 'ox'
3281        return (vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox" || vr === "UT" ||
3282            vr === "SQ" || vr === "UN");
3283    };
3284
3285    /**
3286     * Does this tag have a VR.
3287     * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags.
3288     * @param {String} group The tag group.
3289     * @param {String} element The tag element.
3290     * @returns {Boolean} True if this tar has a VR.
3291     */
3292    dwv.dicom.isTagWithVR = function(group, element) {
3293        return !(group === "0xFFFE" &&
3294            (element === "0xE000" || element === "0xE00D" || element === "0xE0DD"));
3295    };
3296
3297
3298    /**
3299     * Get the number of bytes occupied by a data element prefix, i.e. without its value.
3300     * @param {String} vr The Value Representation of the element.
3301     * @param {Boolean} isImplicit Does the data use implicit VR?
3302     * WARNING: this is valid for tags with a VR, if not sure use the 'isTagWithVR' function first.
3303     * Reference:
3304     * - [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1},
3305     * - [Data Element implicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.5.html#table_7.5-1}.
3306     *
3307     * | Tag | VR  | VL | Value |
3308     * | 4   | 2   | 2  | X     | -> regular explicit: 8 + X
3309     * | 4   | 2+2 | 4  | X     | -> 32bit VL: 12 + X
3310     *
3311     * | Tag | VL | Value |
3312     * | 4   | 4  | X     | -> implicit (32bit VL): 8 + X
3313     *
3314     * | Tag | Len | Value |
3315     * | 4   | 4   | X     | -> item: 8 + X
3316     */
3317    dwv.dicom.getDataElementPrefixByteSize = function(vr, isImplicit) {
3318        return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8;
3319    };
3320
3321    /**
3322     * DicomParser class.
3323     * @constructor
3324     */
3325    dwv.dicom.DicomParser = function() {
3326        /**
3327         * The list of DICOM elements.
3328         * @type Array
3329         */
3330        this.dicomElements = {};
3331
3332        /**
3333         * Default character set (optional).
3334         * @private
3335         * @type String
3336         */
3337        var defaultCharacterSet;
3338        /**
3339         * Get the default character set.
3340         * @return {String} The default character set.
3341         */
3342        this.getDefaultCharacterSet = function() {
3343            return defaultCharacterSet;
3344        };
3345        /**
3346         * Set the default character set.
3347         * param {String} The character set.
3348         */
3349        this.setDefaultCharacterSet = function(characterSet) {
3350            defaultCharacterSet = characterSet;
3351        };
3352    };
3353
3354    /**
3355     * Get the raw DICOM data elements.
3356     * @return {Object} The raw DICOM elements.
3357     */
3358    dwv.dicom.DicomParser.prototype.getRawDicomElements = function() {
3359        return this.dicomElements;
3360    };
3361
3362    /**
3363     * Get the DICOM data elements.
3364     * @return {Object} The DICOM elements.
3365     */
3366    dwv.dicom.DicomParser.prototype.getDicomElements = function() {
3367        return new dwv.dicom.DicomElementsWrapper(this.dicomElements);
3368    };
3369
3370    /**
3371     * Read a DICOM tag.
3372     * @param reader The raw data reader.
3373     * @param offset The offset where to start to read.
3374     * @return An object containing the tags 'group', 'element' and 'name'.
3375     */
3376    dwv.dicom.DicomParser.prototype.readTag = function(reader, offset) {
3377        // group
3378        var group = reader.readHex(offset);
3379        offset += Uint16Array.BYTES_PER_ELEMENT;
3380        // element
3381        var element = reader.readHex(offset);
3382        offset += Uint16Array.BYTES_PER_ELEMENT;
3383        // name
3384        var name = dwv.dicom.getGroupElementKey(group, element);
3385        // return
3386        return {
3387            'group': group,
3388            'element': element,
3389            'name': name,
3390            'endOffset': offset
3391        };
3392    };
3393
3394    /**
3395     * Read an item data element.
3396     * @param {Object} reader The raw data reader.
3397     * @param {Number} offset The offset where to start to read.
3398     * @param {Boolean} implicit Is the DICOM VR implicit?
3399     * @returns {Object} The item data as a list of data elements.
3400     */
3401    dwv.dicom.DicomParser.prototype.readItemDataElement = function(reader, offset, implicit) {
3402        var itemData = {};
3403
3404        // read the first item
3405        var item = this.readDataElement(reader, offset, implicit);
3406        offset = item.endOffset;
3407
3408        // exit if it is a sequence delimitation item
3409        var isSeqDelim = (item.tag.name === "xFFFEE0DD");
3410        if (isSeqDelim) {
3411            return {
3412                data: itemData,
3413                endOffset: item.endOffset,
3414                isSeqDelim: isSeqDelim
3415            };
3416        }
3417
3418        // store it
3419        itemData[item.tag.name] = item;
3420
3421        // explicit VR items
3422        if (item.vl !== "u/l") {
3423            // not empty
3424            if (item.vl !== 0) {
3425                // read until the end offset
3426                var endOffset = offset;
3427                offset -= item.vl;
3428                while (offset < endOffset) {
3429                    item = this.readDataElement(reader, offset, implicit);
3430                    offset = item.endOffset;
3431                    itemData[item.tag.name] = item;
3432                }
3433            }
3434        }
3435        // implicit VR items
3436        else {
3437            // read until the item delimitation item
3438            var isItemDelim = false;
3439            while (!isItemDelim) {
3440                item = this.readDataElement(reader, offset, implicit);
3441                offset = item.endOffset;
3442                isItemDelim = (item.tag.name === "xFFFEE00D");
3443                if (!isItemDelim) {
3444                    itemData[item.tag.name] = item;
3445                }
3446            }
3447        }
3448
3449        return {
3450            'data': itemData,
3451            'endOffset': offset,
3452            'isSeqDelim': false
3453        };
3454    };
3455
3456    /**
3457     * Read the pixel item data element.
3458     * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}.
3459     * @param {Object} reader The raw data reader.
3460     * @param {Number} offset The offset where to start to read.
3461     * @param {Boolean} implicit Is the DICOM VR implicit?
3462     * @returns {Array} The item data as an array of data elements.
3463     */
3464    dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function(reader, offset, implicit) {
3465        var itemData = [];
3466
3467        // first item: basic offset table
3468        var item = this.readDataElement(reader, offset, implicit);
3469        var offsetTableVl = item.vl;
3470        offset = item.endOffset;
3471
3472        // read until the sequence delimitation item
3473        var isSeqDelim = false;
3474        while (!isSeqDelim) {
3475            item = this.readDataElement(reader, offset, implicit);
3476            offset = item.endOffset;
3477            isSeqDelim = (item.tag.name === "xFFFEE0DD");
3478            if (!isSeqDelim) {
3479                itemData.push(item.value);
3480            }
3481        }
3482
3483        return {
3484            'data': itemData,
3485            'endOffset': offset,
3486            'offsetTableVl': offsetTableVl
3487        };
3488    };
3489
3490    /**
3491     * Read a DICOM data element.
3492     * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}.
3493     * @param {Object} reader The raw data reader.
3494     * @param {Number} offset The offset where to start to read.
3495     * @param {Boolean} implicit Is the DICOM VR implicit?
3496     * @return {Object}
3496 An object containing the element 'tag', 'vl', 'vr', 'data' and 'endOffset'.
3497     */
3498    dwv.dicom.DicomParser.prototype.readDataElement = function(reader, offset, implicit) {
3499        // Tag: group, element
3500        var tag = this.readTag(reader, offset);
3501        offset = tag.endOffset;
3502
3503        // Value Representation (VR)
3504        var vr = null;
3505        var is32bitVLVR = false;
3506        if (dwv.dicom.isTagWithVR(tag.group, tag.element)) {
3507            // implicit VR
3508            if (implicit) {
3509                vr = "UN";
3510                var dict = dwv.dicom.dictionary;
3511                if (typeof dict[tag.group] !== "undefined" &&
3512                    typeof dict[tag.group][tag.element] !== "undefined") {
3513                    vr = dwv.dicom.dictionary[tag.group][tag.element][0];
3514                }
3515                is32bitVLVR = true;
3516            } else {
3517                vr = reader.readString(offset, 2);
3518                offset += 2 * Uint8Array.BYTES_PER_ELEMENT;
3519                is32bitVLVR = dwv.dicom.is32bitVLVR(vr);
3520                // reserved 2 bytes
3521                if (is32bitVLVR) {
3522                    offset += 2 * Uint8Array.BYTES_PER_ELEMENT;
3523                }
3524            }
3525        } else {
3526            vr = "UN";
3527            is32bitVLVR = true;
3528        }
3529
3530        // Value Length (VL)
3531        var vl = 0;
3532        if (is32bitVLVR) {
3533            vl = reader.readUint32(offset);
3534            offset += Uint32Array.BYTES_PER_ELEMENT;
3535        } else {
3536            vl = reader.readUint16(offset);
3537            offset += Uint16Array.BYTES_PER_ELEMENT;
3538        }
3539
3540        // check the value of VL
3541        var vlString = vl;
3542        if (vl === 0xffffffff) {
3543            vlString = "u/l";
3544            vl = 0;
3545        }
3546
3547        var startOffset = offset;
3548
3549        // data
3550        var data = null;
3551        var isPixelData = (tag.name === "x7FE00010");
3552        // pixel data sequence (implicit)
3553        if (isPixelData && vlString === "u/l") {
3554            var pixItemData = this.readPixelItemDataElement(reader, offset, implicit);
3555            offset = pixItemData.endOffset;
3556            startOffset += pixItemData.offsetTableVl;
3557            data = pixItemData.data;
3558        } else if (isPixelData && (vr === "OB" || vr === "OW" || vr === "OF" || vr === "ox")) {
3559            // BitsAllocated
3560            var bitsAllocated = 16;
3561            if (typeof this.dicomElements.x00280100 !== 'undefined') {
3562                bitsAllocated = this.dicomElements.x00280100.value[0];
3563            } else {
3564                console.warn("Reading DICOM pixel data with default bitsAllocated.");
3565            }
3566            if (bitsAllocated === 8 && vr === "OW") {
3567                console.warn("Reading DICOM pixel data with vr=OW and bitsAllocated=8 (should be 16).");
3568            }
3569            if (bitsAllocated === 16 && vr === "OB") {
3570                console.warn("Reading DICOM pixel data with vr=OB and bitsAllocated=16 (should be 8).");
3571            }
3572            // PixelRepresentation 0->unsigned, 1->signed
3573            var pixelRepresentation = 0;
3574            if (typeof this.dicomElements.x00280103 !== 'undefined') {
3575                pixelRepresentation = this.dicomElements.x00280103.value[0];
3576            }
3577            // read
3578            if (bitsAllocated === 8) {
3579                if (pixelRepresentation === 0) {
3580                    data = reader.readUint8Array(offset, vl);
3581                } else {
3582                    data = reader.readInt8Array(offset, vl);
3583                }
3584            } else if (bitsAllocated === 16) {
3585                if (pixelRepresentation === 0) {
3586                    data = reader.readUint16Array(offset, vl);
3587                } else {
3588                    data = reader.readInt16Array(offset, vl);
3589                }
3590            } else if (bitsAllocated === 32) {
3591                if (pixelRepresentation === 0) {
3592                    data = reader.readUint32Array(offset, vl);
3593                } else {
3594                    data = reader.readInt32Array(offset, vl);
3595                }
3596            } else if (bitsAllocated === 64) {
3597                if (pixelRepresentation === 0) {
3598                    data = reader.readUint64Array(offset, vl);
3599                } else {
3600                    data = reader.readInt64Array(offset, vl);
3601                }
3602            }
3603            offset += vl;
3604        }
3605        // others
3606        else if (vr === "OB") {
3607            data = reader.readInt8Array(offset, vl);
3608            offset += vl;
3609        } else if (vr === "OW") {
3610            data = reader.readInt16Array(offset, vl);
3611            offset += vl;
3612        } else if (vr === "OF") {
3613            data = reader.readInt32Array(offset, vl);
3614            offset += vl;
3615        } else if (vr === "OD") {
3616            data = reader.readInt64Array(offset, vl);
3617            offset += vl;
3618        }
3619        // numbers
3620        else if (vr === "US") {
3621            data = reader.readUint16Array(offset, vl);
3622            offset += vl;
3623        } else if (vr === "UL") {
3624            data = reader.readUint32Array(offset, vl);
3625            offset += vl;
3626        } else if (vr === "SS") {
3627            data = reader.readInt16Array(offset, vl);
3628            offset += vl;
3629        } else if (vr === "SL") {
3630            data = reader.readInt32Array(offset, vl);
3631            offset += vl;
3632        } else if (vr === "FL") {
3633            data = reader.readFloat32Array(offset, vl);
3634            offset += vl;
3635        } else if (vr === "FD") {
3636            data = reader.readFloat64Array(offset, vl);
3637            offset += vl;
3638        }
3639        // attribute
3640        else if (vr === "AT") {
3641            var raw = reader.readUint16Array(offset, vl);
3642            offset += vl;
3643            data = [];
3644            for (var i = 0, leni = raw.length; i < leni; i += 2) {
3645                var stri = raw[i].toString(16);
3646                var stri1 = raw[i + 1].toString(16);
3647                var str = "(";
3648                str += "0000".substr(0, 4 - stri.length) + stri.toUpperCase();
3649                str += ",";
3650                str += "0000".substr(0, 4 - stri1.length) + stri1.toUpperCase();
3651                str += ")";
3652                data.push(str);
3653            }
3654        }
3655        // not available
3656        else if (vr === "UN") {
3657            data = reader.readUint8Array(offset, vl);
3658            offset += vl;
3659        }
3660        // sequence
3661        else if (vr === "SQ") {
3662            data = [];
3663            var itemData;
3664            // explicit VR sequence
3665            if (vlString !== "u/l") {
3666                // not empty
3667                if (vl !== 0) {
3668                    var sqEndOffset = offset + vl;
3669                    while (offset < sqEndOffset) {
3670                        itemData = this.readItemDataElement(reader, offset, implicit);
3671                        data.push(itemData.data);
3672                        offset = itemData.endOffset;
3673                    }
3674                }
3675            }
3676            // implicit VR sequence
3677            else {
3678                // read until the sequence delimitation item
3679                var isSeqDelim = false;
3680                while (!isSeqDelim) {
3681                    itemData = this.readItemDataElement(reader, offset, implicit);
3682                    isSeqDelim = itemData.isSeqDelim;
3683                    offset = itemData.endOffset;
3684                    // do not store the delimitation item
3685                    if (!isSeqDelim) {
3686                        data.push(itemData.data);
3687                    }
3688                }
3689            }
3690        }
3691        // raw
3692        else {
3693            if (vr === "SH" || vr === "LO" || vr === "ST" ||
3694                vr === "PN" || vr === "LT" || vr === "UT") {
3695                data = reader.readSpecialString(offset, vl);
3696            } else {
3697                data = reader.readString(offset, vl);
3698            }
3699            offset += vl;
3700            data = data.split("\\");
3701        }
3702
3703        // return
3704        return {
3705            'tag': tag,
3706            'vr': vr,
3707            'vl': vlString,
3708            'value': data,
3709            'startOffset': startOffset,
3710            'endOffset': offset
3711        };
3712    };
3713
3714    /**
3715     * Parse the complete DICOM file (given as input to the class).
3716     * Fills in the member object 'dicomElements'.
3717     * @param buffer The input array buffer.
3718     */
3719    dwv.dicom.DicomParser.prototype.parse = function(buffer) {
3720        var offset = 0;
3721        var implicit = false;
3722        // default readers
3723        var metaReader = new dwv.dicom.DataReader(buffer);
3724        var dataReader = new dwv.dicom.DataReader(buffer);
3725
3726        // 128 -> 132: magic word
3727        offset = 128;
3728        var magicword = metaReader.readString(offset, 4);
3729        offset += 4 * Uint8Array.BYTES_PER_ELEMENT;
3730        if (magicword !== "DICM") {
3731            throw new Error("Not a valid DICOM file (no magic DICM word found)");
3732        }
3733
3734        // 0x0002, 0x0000: FileMetaInformationGroupLength
3735        var dataElement = this.readDataElement(metaReader, offset, false);
3736        offset = dataElement.endOffset;
3737        // store the data element
3738        this.dicomElements[dataElement.tag.name] = dataElement;
3739        // get meta length
3740        var metaLength = parseInt(dataElement.value[0], 10);
3741
3742        // meta elements
3743        var metaEnd = offset + metaLength;
3744        while (offset < metaEnd) {
3745            // get the data element
3746            dataElement = this.readDataElement(metaReader, offset, false);
3747            offset = dataElement.endOffset;
3748            // store the data element
3749            this.dicomElements[dataElement.tag.name] = dataElement;
3750        }
3751
3752        // check the TransferSyntaxUID (has to be there!)
3753        if (typeof this.dicomElements.x00020010 === "undefined") {
3754            throw new Error("Not a valid DICOM file (no TransferSyntaxUID found)");
3755        }
3756        var syntax = dwv.dicom.cleanString(this.dicomElements.x00020010.value[0]);
3757
3758        // check support
3759        if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) {
3760            throw new Error("Unsupported DICOM transfer syntax: '" + syntax +
3761                "' (" + dwv.dicom.getTransferSyntaxName(syntax) + ")");
3762        }
3763
3764        // Implicit VR
3765        if (dwv.dicom.isImplicitTransferSyntax(syntax)) {
3766            implicit = true;
3767        }
3768
3769        // Big Endian
3770        if (dwv.dicom.isBigEndianTransferSyntax(syntax)) {
3771            dataReader = new dwv.dicom.DataReader(buffer, false);
3772        }
3773
3774        // default character set
3775        if (typeof this.getDefaultCharacterSet() !== "undefined") {
3776            dataReader.setUtfLabel(this.getDefaultCharacterSet()
3776);
3777        }
3778
3779        // DICOM data elements
3780        while (offset < buffer.byteLength) {
3781            // get the data element
3782            dataElement = this.readDataElement(dataReader, offset, implicit);
3783            // check character set
3784            if (dataElement.tag.name === "x00080005") {
3785                var charSetTerm;
3786                if (dataElement.value.length === 1) {
3787                    charSetTerm = dwv.dicom.cleanString(dataElement.value[0]);
3788                } else {
3789                    charSetTerm = dwv.dicom.cleanString(dataElement.value[1]);
3790                    console.warn("Unsupported character set with code extensions: '" + charSetTerm + "'.");
3791                }
3792                dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm));
3793            }
3794            // increment offset
3795            offset = dataElement.endOffset;
3796            // store the data element
3797            this.dicomElements[dataElement.tag.name] = dataElement;
3798        }
3799
3800        // safety check...
3801        if (buffer.byteLength !== offset) {
3802            console.warn("Did not reach the end of the buffer: " +
3803                offset + " != " + buffer.byteLength);
3804        }
3805
3806        // pixel buffer
3807        if (typeof this.dicomElements.x7FE00010 !== "undefined") {
3808
3809            var numberOfFrames = 1;
3810            if (typeof this.dicomElements.x00280008 !== "undefined") {
3811                numberOfFrames = this.dicomElements.x00280008.value[0];
3812            }
3813
3814            if (this.dicomElements.x7FE00010.vl !== "u/l") {
3815                // compressed should be encapsulated...
3816                if (dwv.dicom.isJpeg2000TransferSyntax(syntax) ||
3817                    dwv.dicom.isJpegBaselineTransferSyntax(syntax) ||
3818                    dwv.dicom.isJpegLosslessTransferSyntax(syntax)) {
3819                    console.warn("Compressed but no items...");
3820                }
3821
3822                // calculate the slice size
3823                var pixData = this.dicomElements.x7FE00010.value;
3824                var columns = this.dicomElements.x00280011.value[0];
3825                var rows = this.dicomElements.x00280010.value[0];
3826                var samplesPerPixel = this.dicomElements.x00280002.value[0];
3827                var sliceSize = columns * rows * samplesPerPixel;
3828                // slice data in an array of frames
3829                var newPixData = [];
3830                var frameOffset = 0;
3831                for (var g = 0; g < numberOfFrames; ++g) {
3832                    newPixData[g] = pixData.slice(frameOffset, frameOffset + sliceSize);
3833                    frameOffset += sliceSize;
3834                }
3835                // store as pixel data
3836                this.dicomElements.x7FE00010.value = newPixData;
3837            } else {
3838                // handle fragmented pixel buffer
3839                // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html
3840                // (third note, "Depending on the transfer syntax...")
3841                var pixItems = this.dicomElements.x7FE00010.value;
3842                if (pixItems.length > 1 && pixItems.length > numberOfFrames) {
3843
3844                    // concatenate pixel data items
3845                    // concat does not work on typed arrays
3846                    //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data );
3847                    // manual concat...
3848                    var nItemPerFrame = pixItems.length / numberOfFrames;
3849                    var newPixItems = [];
3850                    var index = 0;
3851                    for (var f = 0; f < numberOfFrames; ++f) {
3852                        index = f * nItemPerFrame;
3853                        // calculate the size of a frame
3854                        var size = 0;
3855                        for (var i = 0; i < nItemPerFrame; ++i) {
3856                            size += pixItems[index + i].length;
3857                        }
3858                        // create new buffer
3859                        var newBuffer = new pixItems[0].constructor(size);
3860                        // fill new buffer
3861                        var fragOffset = 0;
3862                        for (var j = 0; j < nItemPerFrame; ++j) {
3863                            newBuffer.set(pixItems[index + j], fragOffset);
3864                            fragOffset += pixItems[index + j].length;
3865                        }
3866                        newPixItems[f] = newBuffer;
3867                    }
3868                    // store as pixel data
3869                    this.dicomElements.x7FE00010.value = newPixItems;
3870                }
3871            }
3872        }
3873    };
3874
3875    /**
3876     * DicomElements wrapper.
3877     * @constructor
3878     * @param {Array} dicomElements The elements to wrap.
3879     */
3880    dwv.dicom.DicomElementsWrapper = function(dicomElements) {
3881
3882        /**
3883         * Get a DICOM Element value from a group/element key.
3884         * @param {String} groupElementKey The key to retrieve.
3885         * @return {Object} The DICOM element.
3886         */
3887        this.getDEFromKey = function(groupElementKey) {
3888            return dicomElements[groupElementKey];
3889        };
3890
3891        /**
3892         * Get a DICOM Element value from a group/element key.
3893         * @param {String} groupElementKey The key to retrieve.
3894         * @param {Boolean} asArray Get the value as an Array.
3895         * @return {Object} The DICOM element value.
3896         */
3897        this.getFromKey = function(groupElementKey, asArray) {
3898            // default
3899            if (typeof asArray === "undefined") {
3900                asArray = false;
3901            }
3902            var value = null;
3903            var dElement = dicomElements[groupElementKey];
3904            if (typeof dElement !== "undefined") {
3905                // raw value if only one
3906                if (dElement.value.length === 1 && asArray === false) {
3907                    value = dElement.value[0];
3908                } else {
3909                    value = dElement.value;
3910                }
3911            }
3912            return value;
3913        };
3914
3915        /**
3916         * Dump the DICOM tags to an array.
3917         * @return {Array}
3918         */
3919        this.dumpToTable = function() {
3920            var keys = Object.keys(dicomElements);
3921            var dict = dwv.dicom.dictionary;
3922            var table = [];
3923            var dicomElement = null;
3924            var dictElement = null;
3925            var row = null;
3926            for (var i = 0, leni = keys.length; i < leni; ++i) {
3927                dicomElement = dicomElements[keys[i]];
3928                row = {};
3929                // dictionnary entry (to get name)
3930                dictElement = null;
3931                if (typeof dict[dicomElement.tag.group] !== "undefined" &&
3932                    typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") {
3933                    dictElement = dict[dicomElement.tag.group][dicomElement.tag.element];
3934                }
3935                // name
3936                if (dictElement !== null) {
3937                    row.name = dictElement[2];
3938                } else {
3939                    row.name = "Unknown Tag & Data";
3940                }
3941                // value
3942                row.value = this.getElementValueAsString(dicomElement);
3943                // others
3944                row.group = dicomElement.tag.group;
3945                row.element = dicomElement.tag.element;
3946                row.vr = dicomElement.vr;
3947                row.vl = dicomElement.vl;
3948
3949                table.push(row);
3950            }
3951            return table;
3952        };
3953
3954        /**
3955         * Dump the DICOM tags to a string.
3956         * @return {String} The dumped file.
3957         */
3958        this.dump = function() {
3959            var keys = Object.keys(dicomElements);
3960            var result = "\n";
3961            result += "# Dicom-File-Format\n";
3962            result += "\n";
3963            result += "# Dicom-Meta-Information-Header\n";
3964            result += "# Used TransferSyntax: ";
3965            if (dwv.dicom.isNativeLittleEndian()) {
3966                result += "Little Endian Explicit\n";
3967            } else {
3968                result += "NOT Little Endian Explicit\n";
3969            }
3970            var dicomElement = null;
3971            var checkHeader = true;
3972            for (var i = 0, leni = keys.length; i < leni; ++i) {
3973                dicomElement = dicomElements[keys[i]];
3974                if (checkHeader && dicomElement.tag.group !== "0x0002") {
3975                    result += "\n";
3976                    result += "# Dicom-Data-Set\n";
3977                    result += "# Used TransferSyntax: ";
3978                    var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]);
3979                    result += dwv.dicom.getTransferSyntaxName(syntax);
3980                    result += "\n";
3981                    checkHeader = false;
3982                }
3983                result += this.getElementAsString(dicomElement) + "\n";
3984            }
3985            return result;
3986        };
3987
3988    };
3989
3990    /**
3991     * Get a data element value as a string.
3992     * @param {Object} dicomElement The DICOM element.
3993     * @param {Boolean} pretty When set to true, returns a 'pretified' content.
3994     * @return {String} A string representation of the DICOM element.
3995     */
3996    dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function(dicomElement, pretty
3996) {
3997        var str = "";
3998        var strLenLimit = 65;
3999
4000        // dafault to pretty output
4001        if (typeof pretty === "undefined") {
4002            pretty = true;
4003        }
4004        // check dicom element input
4005        if (typeof dicomElement === "undefined" || dicomElement === null) {
4006            return str;
4007        }
4008
4009        // Polyfill for Number.isInteger.
4010        var isInteger = Number.isInteger || function(value) {
4011            return typeof value === 'number' &&
4012                isFinite(value) &&
4013                Math.floor(value) === value;
4014        };
4015
4016        // TODO Support sequences.
4017
4018        if (dicomElement.vr !== "SQ" &&
4019            dicomElement.value.length === 1 && dicomElement.value[0] === "") {
4020            str += "(no value available)";
4021        } else if (dicomElement.tag.group === '0x7FE0' &&
4022            dicomElement.tag.element === '0x0010' &&
4023            dicomElement.vl === 'u/l') {
4024            str = "(PixelSequence)";
4025        } else if (dicomElement.vr === "DA" && pretty) {
4026            var daValue = dicomElement.value[0];
4027            var daYear = parseInt(daValue.substr(0, 4), 10);
4028            var daMonth = parseInt(daValue.substr(4, 2), 10) - 1; // 0-11
4029            var daDay = parseInt(daValue.substr(6, 2), 10);
4030            var da = new Date(daYear, daMonth, daDay);
4031            str = da.toLocaleDateString();
4032        } else if (dicomElement.vr === "TM" && pretty) {
4033            var tmValue = dicomElement.value[0];
4034            var tmHour = tmValue.substr(0, 2);
4035            var tmMinute = tmValue.length >= 4 ? tmValue.substr(2, 2) : "00";
4036            var tmSeconds = tmValue.length >= 6 ? tmValue.substr(4, 2) : "00";
4037            str = tmHour + ':' + tmMinute + ':' + tmSeconds;
4038        } else {
4039            var isOtherVR = (dicomElement.vr[0].toUpperCase() === "O");
4040            var isFloatNumberVR = (dicomElement.vr === "FL" ||
4041                dicomElement.vr === "FD" ||
4042                dicomElement.vr === "DS");
4043            var valueStr = "";
4044            for (var k = 0, lenk = dicomElement.value.length; k < lenk; ++k) {
4045                valueStr = "";
4046                if (k !== 0) {
4047                    valueStr += "\\";
4048                }
4049                if (isFloatNumberVR) {
4050                    var val = dicomElement.value[k];
4051                    if (typeof val === "string") {
4052                        val = dwv.dicom.cleanString(val);
4053                    }
4054                    var num = Number(val);
4055                    if (!isInteger(num) && pretty) {
4056                        valueStr += num.toPrecision(4);
4057                    } else {
4058                        valueStr += num.toString();
4059                    }
4060                } else if (isOtherVR) {
4061                    var tmp = dicomElement.value[k].toString(16);
4062                    if (dicomElement.vr === "OB") {
4063                        tmp = "00".substr(0, 2 - tmp.length) + tmp;
4064                    } else {
4065                        tmp = "0000".substr(0, 4 - tmp.length) + tmp;
4066                    }
4067                    valueStr += tmp;
4068                } else if (typeof dicomElement.value[k] === "string") {
4069                    valueStr += dwv.dicom.cleanString(dicomElement.value[k]);
4070                } else {
4071                    valueStr += dicomElement.value[k];
4072                }
4073                // check length
4074                if (str.length + valueStr.length <= strLenLimit) {
4075                    str += valueStr;
4076                } else {
4077                    str += "...";
4078                    break;
4079                }
4080            }
4081        }
4082        return str;
4083    };
4084
4085    /**
4086     * Get a data element value as a string.
4087     * @param {String} groupElementKey The key to retrieve.
4088     */
4089    dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsStringFromKey = function(groupElementKey) {
4090        return this.getElementValueAsString(this.getDEFromKey(groupElementKey));
4091    };
4092
4093    /**
4094     * Get a data element as a string.
4095     * @param {Object} dicomElement The DICOM element.
4096     * @param {String}
4096 prefix A string to prepend this one.
4097     */
4098    dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function(dicomElement, prefix) {
4099        // default prefix
4100        prefix = prefix || "";
4101
4102        // get element from dictionary
4103        var dict = dwv.dicom.dictionary;
4104        var dictElement = null;
4105        if (typeof dict[dicomElement.tag.group] !== "undefined" &&
4106            typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== "undefined") {
4107            dictElement = dict[dicomElement.tag.group][dicomElement.tag.element];
4108        }
4109
4110        var deSize = dicomElement.value.length;
4111        var isOtherVR = (dicomElement.vr[0].toUpperCase() === "O");
4112
4113        // no size for delimitations
4114        if (dicomElement.tag.group === "0xFFFE" && (
4115                dicomElement.tag.element === "0xE00D" ||
4116                dicomElement.tag.element === "0xE0DD")) {
4117            deSize = 0;
4118        } else if (isOtherVR) {
4119            deSize = 1;
4120        }
4121
4122        var isPixSequence = (dicomElement.tag.group === '0x7FE0' &&
4123            dicomElement.tag.element === '0x0010' &&
4124            dicomElement.vl === 'u/l');
4125
4126        var line = null;
4127
4128        // (group,element)
4129        line = "(";
4130        line += dicomElement.tag.group.substr(2, 5).toLowerCase();
4131        line += ",";
4132        line += dicomElement.tag.element.substr(2, 5).toLowerCase();
4133        line += ") ";
4134        // value representation
4135        line += dicomElement.vr;
4136        // value
4137        if (dicomElement.vr !== "SQ" && dicomElement.value.length === 1 && dicomElement.value[0] === "") {
4138            line += " (no value available)";
4139            deSize = 0;
4140        } else {
4141            // simple number display
4142            if (dicomElement.vr === "na") {
4143                line += " ";
4144                line += dicomElement.value[0];
4145            }
4146            // pixel sequence
4147            else if (isPixSequence) {
4148                line += " (PixelSequence #=" + deSize + ")";
4149            } else if (dicomElement.vr === 'SQ') {
4150                line += " (Sequence with";
4151                if (dicomElement.vl === "u/l") {
4152                    line += " undefined";
4153                } else {
4154                    line += " explicit";
4155                }
4156                line += " length #=";
4157                line += dicomElement.value.length;
4158                line += ")";
4159            }
4160            // 'O'ther array, limited display length
4161            else if (isOtherVR ||
4162                dicomElement.vr === 'pi' ||
4163                dicomElement.vr === "UL" ||
4164                dicomElement.vr === "US" ||
4165                dicomElement.vr === "SL" ||
4166                dicomElement.vr === "SS" ||
4167                dicomElement.vr === "FL" ||
4168                dicomElement.vr === "FD" ||
4169                dicomElement.vr === "AT") {
4170                line += " ";
4171                line += this.getElementValueAsString(dicomElement, false);
4172            }
4173            // default
4174            else {
4175                line += " [";
4176                line += this.getElementValueAsString(dicomElement, false);
4177                line += "]";
4178            }
4179        }
4180
4181        // align #
4182        var nSpaces = 55 - line.length;
4183        if (nSpaces > 0) {
4184            for (var s = 0; s < nSpaces; ++s) {
4185                line += " ";
4186            }
4187        }
4188        line += " # ";
4189        if (dicomElement.vl < 100) {
4190            line += " ";
4191        }
4192        if (dicomElement.vl < 10) {
4193            line += " ";
4194        }
4195        line += dicomElement.vl;
4196        line += ", ";
4197        line += deSize; //dictElement[1];
4198        line += " ";
4199        if (dictElement !== null) {
4200            line += dictElement[2];
4201        } else {
4202            line += "Unknown Tag & Data";
4203        }
4204
4205        var message = null;
4206
4207        // continue for sequence
4208        if (dicomElement.vr === 'SQ') {
4209            var item = null;
4210            for (var l = 0, lenl = dicomElement.value.length; l < lenl; ++l) {
4211                item = dicomElement.value[l];
4212                var itemKeys = Object.keys(item);
4213                if (itemKeys.length === 0) {
4214                    continue;
4215                }
4216
4217                // get the item element
4218                var itemElement = item.xFFFEE000;
4219                message = "(Item with";
4220                if (itemElement.vl === "u/l") {
4221                    message += " undefined";
4222                } else {
4223                    message += " explicit";
4224                }
4225                message += " length #=" + (itemKeys.length - 1) + ")";
4226                itemElement.value = [message];
4227                itemElement.vr = "na";
4228
4229                line += "\n";
4230                line += this.getElementAsString(itemElement, prefix + "  ");
4231
4232                for (var m = 0, lenm = itemKeys.length; m < lenm; ++m) {
4233                    if (itemKeys[m] !== "xFFFEE000") {
4234                        line += "\n";
4235                        line += this.getElementAsString(item[itemKeys[m]], prefix + "    ");
4236                    }
4237                }
4238
4239                message = "(ItemDelimitationItem";
4240                if (itemElement.vl !== "u/l") {
4241                    message += " for re-encoding";
4242                }
4243                message += ")";
4244                var itemDelimElement = {
4245                    "tag": { "group": "0xFFFE", "element": "0xE00D" },
4246                    "vr": "na",
4247                    "vl": "0",
4248                    "value": [message]
4249                };
4250                line += "\n";
4251                line += this.getElementAsString(itemDelimElement, prefix + "  ");
4252
4253            }
4254
4255            message = "(SequenceDelimitationItem";
4256            if (dicomElement.vl !== "u/l") {
4257                message += " for re-encod.";
4258            }
4259            message += ")";
4260            var sqDelimElement = {
4261                "tag": { "group": "0xFFFE", "element": "0xE0DD" },
4262                "vr": "na",
4263                "vl": "0",
4264                "value": [message]
4265            };
4266            line += "\n";
4267            line += this.getElementAsString(sqDelimElement, prefix);
4268        }
4269        // pixel sequence
4270        else if (isPixSequence) {
4271            var pixItem = null;
4272            for (var n = 0, lenn = dicomElement.value.length; n < lenn; ++n) {
4273                pixItem = dicomElement.value[n];
4274                line += "\n";
4275                pixItem.vr = 'pi';
4276                line += this.getElementAsString(pixItem, prefix + "  ");
4277            }
4278
4279            var pixDelimElement = {
4280                "tag": { "group": "0xFFFE", "element": "0xE0DD" },
4281                "vr": "na",
4282                "vl": "0",
4283                "value": ["(SequenceDelimitationItem)"]
4284            };
4285            line += "\n";
4286            line += this.getElementAsString(pixDelimElement, prefix);
4287        }
4288
4289        return prefix + line;
4290    };
4291
4292    /**
4293     * Get a DICOM Element value from a group and an element.
4294     * @param {Number} group The group.
4295     * @param {Number} element The element.
4296     * @return {Object} The DICOM element value.
4297     */
4298    dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function(
4299        group, element) {
4300        return this.getFromKey(
4301            dwv.dicom.getGroupElementKey(group, element));
4302    };
4303
4304    /**
4305     * Get a DICOM Element value from a tag name.
4306     * Uses the DICOM dictionary.
4307     * @param {String} name The tag name.
4308     * @return {Object} The DICOM element value.
4309     */
4310    dwv.dicom.DicomElementsWrapper.prototype.getFromName = function(name) {
4311        var value = null;
4312        var tagGE = dwv.dicom.getGroupElementFromName(name);
4313        // check that we are not at the end of the dictionary
4314        if (tagGE.group !== null && tagGE.element !== null) {
4315            value = this.getFromKey(dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element));
4316        }
4317        return value;
4318    };
4319
4320    // namespaces
4321    var dwv = dwv || {};
4322    dwv.dicom = dwv.dicom || {};
4323
4324    /**
4325     * Data writer.
4326     *
4327     * Example usage:
4328     *   var parser = new dwv.dicom.DicomParser();
4329     *   parser.parse(this.response);
4330     *
4331     *   var writer = new dwv.dicom.DicomWriter(parser.getRawDicomElements());
4332     *   var blob = new Blob([writer.getBuffer()], {type: 'application/dicom'});
4333     *
4334     *   var element = document.getElementById("download");
4335     *   element.href = URL.createObjectURL(blob);
4336     *   element.download = "anonym.dcm";
4337     *
4338     * @constructor
4339     * @param {Array} buffer The input array buffer.
4340     * @param {Boolean} isLittleEndian Flag to tell if the data is little or big endian.
4341     */
4342    dwv.dicom.DataWriter = function(buffer, isLittleEndian) {
4343        // Set endian flag if not defined.
4344        if (typeof isLittleEndian === 'undefined') {
4345            isLittleEndian = true;
4346        }
4347
4348        // private DataView
4349        var view = new DataView(buffer);
4350
4351        /**
4352         * Write Uint8 data.
4353         * @param {Number} byteOffset The offset to start writing from.
4354         * @param {Number} value The data to write.
4355         * @returns {Number} The new offset position.
4356         */
4357        this.writeUint8 = function(byteOffset, value) {
4358            view.setUint8(byteOffset, value);
4359            return byteOffset + Uint8Array.BYTES_PER_ELEMENT;
4360        };
4361
4362        /**
4363         * Write Int8 data.
4364         * @param {Number} byteOffset The offset to start writing from.
4365         * @param {Number} value The data to write.
4366         * @returns {Number} The new offset position.
4367         */
4368        this.writeInt8 = function(byteOffset, value) {
4369            view.setInt8(byteOffset, value);
4370            return byteOffset + Int8Array.BYTES_PER_ELEMENT;
4371        };
4372
4373        /**
4374         * Write Uint16 data.
4375         * @param {Number} byteOffset The offset to start writing from.
4376         * @param {Number} value The data to write.
4377         * @returns {Number} The new offset position.
4378         */
4379        this.writeUint16 = function(byteOffset, value) {
4380            view.setUint16(byteOffset, value, isLittleEndian);
4381            return byteOffset + Uint16Array.BYTES_PER_ELEMENT;
4382        };
4383
4384        /**
4385         * Write Int16 data.
4386         * @param {Number} byteOffset The offset to start writing from.
4387         * @param {Number} value The data to write.
4388         * @returns {Number} The new offset position.
4389         */
4390        this.writeInt16 = function(byteOffset, value) {
4391            view.setInt16(byteOffset, value, isLittleEndian);
4392            return byteOffset + Int16Array.BYTES_PER_ELEMENT;
4393        };
4394
4395        /**
4396         * Write Uint32 data.
4397         * @param {Number} byteOffset The offset to start writing from.
4398         * @param {Number} value The data to write.
4399         * @returns {Number} The new offset position.
4400         */
4401        this.writeUint32 = function(byteOffset, value) {
4402            view.setUint32(byteOffset, value, isLittleEndian);
4403            return byteOffset + Uint32Array.BYTES_PER_ELEMENT;
4404        };
4405
4406        /**
4407         * Write Int32 data.
4408         * @param {Number} byteOffset The offset to start writing from.
4409         * @param {Number} value The data to write.
4410         * @returns {Number} The new offset position.
4411         */
4412        this.writeInt32 = function(byteOffset, value) {
4413            view.setInt32(byteOffset, value, isLittleEndian);
4414            return byteOffset + Int32Array.BYTES_PER_ELEMENT;
4415        };
4416
4417        /**
4418         * Write Float32 data.
4419         * @param {Number} byteOffset The offset to start writing from.
4420         * @param {Number} value The data to write.
4421         * @returns {Number} The new offset position.
4422         */
4423        this.writeFloat32 = function(byteOffset, value) {
4424            view.setFloat32(byteOffset, value, isLittleEndian);
4425            return byteOffset + Float32Array.BYTES_PER_ELEMENT;
4426        };
4427
4428        /**
4429         * Write Float64 data.
4430         * @param {Number} byteOffset The offset to start writing from.
4431         * @param {Number} value The data to write.
4432         * @returns {Number} The new offset position.
4433         */
4434        this.writeFloat64 = function(byteOffset, value) {
4435            view.setFloat64(byteOffset, value, isLittleEndian);
4436            return byteOffset + Float64Array.BYTES_PER_ELEMENT;
4437        };
4438
4439        /**
4440         * Write string data as hexadecimal.
4441         * @param {Number} byteOffset The offset to start writing from.
4442         * @param {Number} str The padded hexadecimal string to write ('0x####').
4443         * @returns {Number} The new offset position.
4444         */
4445        this.writeHex = function(byteOffset, str) {
4446            // remove first two chars and parse
4447            var value = parseInt(str.substr(2), 16);
4448            view.setUint16(byteOffset, value, isLittleEndian);
4449            return byteOffset + Uint16Array.BYTES_PER_ELEMENT;
4450        };
4451
4452        /**
4453         * Write string data.
4454         * @param {Number} byteOffset The offset to start writing from.
4455         * @param {Number} str The data to write.
4456         * @returns {Number} The new offset position.
4457         */
4458        this.writeString = function(byteOffset, str) {
4459            for (var i = 0, len = str.length; i < len; ++i) {
4460                view.setUint8(byteOffset, str.charCodeAt(i));
4461                byteOffset += Uint8Array.BYTES_PER_ELEMENT;
4462            }
4463            return byteOffset;
4464        };
4465
4466    };
4467
4468    /**
4469     * Write Uint8 array.
4470     * @param {Number} byteOffset The offset to start writing from.
4471     * @param {Array} array The array to write.
4472     * @returns {Number} The new offset position.
4473     */
4474    dwv.dicom.DataWriter.prototype.writeUint8Array = function(byteOffset, array) {
4475        for (var i = 0, len = array.length; i < len; ++i) {
4476            byteOffset = this.writeUint8(byteOffset, array[i]);
4477        }
4478        return byteOffset;
4479    };
4480
4481    /**
4482     * Write Int8 array.
4483     * @param {Number} byteOffset The offset to start writing from.
4484     * @param {Array} array The array to write.
4485     * @returns {Number} The new offset position.
4486     */
4487    dwv.dicom.DataWriter.prototype.writeInt8Array = function(byteOffset, array) {
4488        for (var i = 0, len = array.length; i < len; ++i) {
4489            byteOffset = this.writeInt8(byteOffset, array[i]);
4490        }
4491        return byteOffset;
4492    };
4493
4494    /**
4495     * Write Uint16 array.
4496     * @param {Number} byteOffset The offset to start writing from.
4497     * @param {Array} array The array to write.
4498     * @returns {Number} The new offset position.
4499     */
4500    dwv.dicom.DataWriter.prototype.writeUint16Array = function(byteOffset, array) {
4501        for (var i = 0, len = array.length; i < len; ++i) {
4502            byteOffset = this.writeUint16(byteOffset, array[i]);
4503        }
4504        return byteOffset;
4505    };
4506
4507    /**
4508     * Write Int16 array.
4509     * @param {Number} byteOffset The offset to start writing from.
4510     * @param {Array} array The array to write.
4511     * @returns {Number} The new offset position.
4512     */
4513    dwv.dicom.DataWriter.prototype.writeInt16Array = function(byteOffset, array) {
4514        for (var i = 0, len = array.length; i < len; ++i) {
4515            byteOffset = this.writeInt16(byteOffset, array[i]);
4516        }
4517        return byteOffset;
4518    };
4519
4520    /**
4521     * Write Uint32 array.
4522     * @param {Number} byteOffset The offset to start writing from.
4523     * @param {Array} array The array to write.
4524     * @returns {Number} The new offset position.
4525     */
4526    dwv.dicom.DataWriter.prototype.writeUint32Array = function(byteOffset, array) {
4527        for (var i = 0, len = array.length; i < len; ++i) {
4528            byteOffset = this.writeUint32(byteOffset, array[i]);
4529        }
4530        return byteOffset;
4531    };
4532
4533    /**
4534     * Write Int32 array.
4535     * @param {Number} byteOffset The offset to start writing from.
4536     * @param {Array} array The array to write.
4537     * @returns {Number} The new offset position.
4538     */
4539    dwv.dicom.DataWriter.prototype.writeInt32Array = function(byteOffset, array) {
4540        for (var i = 0, len = array.length; i < len; ++i) {
4541            byteOffset = this.writeInt32(byteOffset, array[i]);
4542        }
4543        return byteOffset;
4544    };
4545
4546    /**
4547     * Write Float32 array.
4548     * @param {Number} byteOffset The offset to start writing from.
4549     * @param {Array} array The array to write.
4550     * @returns {Number} The new offset position.
4551     */
4552    dwv.dicom.DataWriter.prototype.writeFloat32Array = function(byteOffset, array) {
4553        for (var i = 0, len = array.length; i < len; ++i) {
4554            byteOffset = this.writeFloat32(byteOffset, array[i]);
4555        }
4556        return byteOffset;
4557    };
4558
4559    /**
4560     * Write Float64 array.
4561     * @param {Number} byteOffset The offset to start writing from.
4562     * @param {Array} array The array to write.
4563     * @returns {Number} The new offset position.
4564     */
4565    dwv.dicom.DataWriter.prototype.writeFloat64Array = function(byteOffset, array) {
4566        for (var i = 0, len = array.length; i < len; ++i) {
4567            byteOffset = this.writeFloat64(byteOffset, array[i]);
4568        }
4569        return byteOffset;
4570    };
4571
4572    /**
4573     * Write string array.
4574     * @param {Number} byteOffset The offset to start writing from.
4575     * @param {Array} array The array to write.
4576     * @returns {Number} The new offset position.
4577     */
4578    dwv.dicom.DataWriter.prototype.writeStringArray = function(byteOffset, array) {
4579        for (var i = 0, len = array.length; i < len; ++i) {
4580            // separator
4581            if (i !== 0) {
4582                byteOffset = this.writeString(byteOffset, "\\");
4583            }
4584            // value
4585            byteOffset = this.writeString(byteOffset, array[i].toString());
4586        }
4587        return byteOffset;
4588    };
4589
4590    /**
4591     * Write a list of items.
4592     * @param {Number} byteOffset The offset to start writing from.
4593     * @param {Array} items The list of items to write.
4594     * @param {Boolean} isImplicit Is the DICOM VR implicit?
4595     * @returns {Number} The new offset position.
4596     */
4597    dwv.dicom.DataWriter.prototype.writeDataElementItems = function(byteOffset, items, isImplicit) {
4598        var item = null;
4599        for (var i = 0; i < items.length; ++i) {
4600            item = items[i];
4601            var itemKeys = Object.keys(item);
4602            if (itemKeys.length === 0) {
4603                continue;
4604            }
4605            // write item
4606            var itemElement = item.xFFFEE000;
4607            itemElement.value = [];
4608            var implicitLength = (itemElement.vl === "u/l");
4609            if (implicitLength) {
4610                itemElement.vl = 0xffffffff;
4611            }
4612            byteOffset = this.writeDataElement(itemElement, byteOffset, isImplicit);
4613            // write rest
4614            for (var m = 0; m < itemKeys.length; ++m) {
4615                if (itemKeys[m] !== "xFFFEE000" && itemKeys[m] !== "xFFFEE00D") {
4616                    byteOffset = this.writeDataElement(item[itemKeys[m]], byteOffset, isImplicit);
4617                }
4618            }
4619            // item delimitation
4620            if (implicitLength) {
4621                var itemDelimElement = {
4622                    'tag': {
4623                        group: "0xFFFE",
4624                        element: "0xE00D",
4625                        name: "ItemDelimitationItem"
4626                    },
4627                    'vr': "NONE",
4628                    'vl': 0,
4629                    'value': []
4630                };
4631                byteOffset = this.writeDataElement(itemDelimElement, byteOffset, isImplicit);
4632            }
4633        }
4634
4635        // return new offset
4636        return byteOffset;
4637    };
4638
4639    /**
4640     * Write data with a specific Value Representation (VR).
4641     * @param {String} vr The data Value Representation (VR).
4642     * @param {Number} byteOffset The offset to start writing from.
4643     * @param {Array} value The array to write.
4644     * @param {Boolean} isImplicit Is the DICOM VR implicit?
4645     * @returns {Number} The new offset position.
4646     */
4647    dwv.dicom.DataWriter.prototype.writeDataElementValue = function(vr, byteOffset, value, isImplicit) {
4648        // first check input type to know how to write
4649        if (value instanceof Uint8Array) {
4650            byteOffset = this.writeUint8Array(byteOffset, value);
4651        } else if (value instanceof Int8Array) {
4652        
4652    byteOffset = this.writeInt8Array(byteOffset, value);
4653        } else if (value instanceof Uint16Array) {
4654            byteOffset = this.writeUint16Array(byteOffset, value);
4655        } else if (value instanceof Int16Array) {
4656            byteOffset = this.writeInt16Array(byteOffset, value);
4657        } else if (value instanceof Uint32Array) {
4658            byteOffset = this.writeUint32Array(byteOffset, value);
4659        } else if (value instanceof Int32Array) {
4660            byteOffset = this.writeInt32Array(byteOffset, value);
4661        } else {
4662            // switch according to VR if input type is undefined
4663            if (vr === "UN") {
4664                byteOffset = this.writeUint8Array(byteOffset, value);
4665            } else if (vr === "OB") {
4666                byteOffset = this.writeInt8Array(byteOffset, value);
4667            } else if (vr === "OW") {
4668                byteOffset = this.writeInt16Array(byteOffset, value);
4669            } else if (vr === "OF") {
4670                byteOffset = this.writeInt32Array(byteOffset, value);
4671            } else if (vr === "OD") {
4672                byteOffset = this.writeInt64Array(byteOffset, value);
4673            } else if (vr === "US") {
4674                byteOffset = this.writeUint16Array(byteOffset, value);
4675            } else if (vr === "SS") {
4676                byteOffset = this.writeInt16Array(byteOffset, value);
4677            } else if (vr === "UL") {
4678                byteOffset = this.writeUint32Array(byteOffset, value);
4679            } else if (vr === "SL") {
4680                byteOffset = this.writeInt32Array(byteOffset, value);
4681            } else if (vr === "FL") {
4682                byteOffset = this.writeFloat32Array(byteOffset, value);
4683            } else if (vr === "FD") {
4684                byteOffset = this.writeFloat64Array(byteOffset, value);
4685            } else if (vr === "SQ") {
4686                byteOffset = this.writeDataElementItems(byteOffset, value, isImplicit);
4687            } else if (vr === "AT") {
4688                for (var i = 0; i < value.length; ++i) {
4689                    var hexString = value[i] + '';
4690                    var hexString1 = hexString.substring(1, 5);
4691                    var hexString2 = hexString.substring(6, 10);
4692                    var dec1 = parseInt(hexString1, 16);
4693                    var dec2 = parseInt(hexString2, 16);
4694                    var atValue = new Uint16Array([dec1, dec2]);
4695                    byteOffset = this.writeUint16Array(byteOffset, atValue);
4696                }
4697            } else {
4698                byteOffset = this.writeStringArray(byteOffset, value);
4699            }
4700        }
4701        // return new offset
4702        return byteOffset;
4703    };
4704
4705    /**
4706     * Write a pixel data element.
4707     * @param {String} vr The data Value Representation (VR).
4708     * @param {String} vl The data Value Length (VL).
4709     * @param {Number} byteOffset The offset to start writing from.
4710     * @param {Array} value The array to write.
4711     * @param {Boolean} isImplicit Is the DICOM VR implicit?
4712     * @returns {Number} The new offset position.
4713     */
4714    dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function(vr, vl, byteOffset, value, isImplicit) {
4715        // explicit length
4716        if (vl !== "u/l") {
4717            var finalValue = value[0];
4718            // flatten multi frame
4719            if (value.length > 1) {
4720                finalValue = dwv.dicom.flattenArrayOfTypedArrays(value);
4721            }
4722            // write
4723            byteOffset = this.writeDataElementValue(vr, byteOffset, finalValue, isImplicit);
4724        } else {
4725            // pixel data as sequence
4726            var item = {};
4727            // first item: basic offset table
4728            item.xFFFEE000 = {
4729                'tag': {
4730                    group: "0xFFFE",
4731                    element: "0xE000",
4732                    name: "xFFFEE000"
4733                },
4734                'vr': "UN",
4735                'vl': 0,
4736                'value': []
4737            };
4738            // data
4739            for (var i = 0; i < value.length; ++i) {
4740                item[i] = {
4741                    'tag': {
4742                        group: "0xFFFE",
4743                        element: "0xE000",
4744                        name: "xFFFEE000"
4745                    },
4746                    'vr': vr,
4747                    'vl': value[i].length,
4748                    'value': value[i]
4749                };
4750            }
4751            // write
4752            byteOffset = this.writeDataElementItems(byteOffset, [item], isImplicit);
4753        }
4754
4755        // return new offset
4756        return byteOffset;
4757    };
4758
4759    /**
4760     * Write a data element.
4761     * @param {Object} element The DICOM data element to write.
4762     * @param {Number} byteOffset The offset to start writing from.
4763     * @param {Boolean} isImplicit Is the DICOM VR implicit?
4764     * @returns {Number} The new offset position.
4765     */
4766    dwv.dicom.DataWriter.prototype.writeDataElement = function(element, byteOffset, isImplicit) {
4767        var isTagWithVR = dwv.dicom.isTagWithVR(element.tag.group, element.tag.element);
4768        var is32bitVLVR = (isImplicit || !isTagWithVR) ? true : dwv.dicom.is32bitVLVR(element.vr);
4769        // group
4770        byteOffset = this.writeHex(byteOffset, element.tag.group);
4771        // element
4772        byteOffset = this.writeHex(byteOffset, element.tag.element);
4773        // VR
4774        if (isTagWithVR && !isImplicit) {
4775            byteOffset = this.writeString(byteOffset, element.vr);
4776            // reserved 2 bytes for 32bit VL
4777            if (is32bitVLVR) {
4778                byteOffset += 2;
4779            }
4780        }
4781
4782        // update vl for sequence or item with implicit length
4783        var vl = element.vl;
4784        if (dwv.dicom.isImplicitLengthSequence(element) ||
4785            dwv.dicom.isImplicitLengthItem(element) ||
4786            dwv.dicom.isImplicitLengthPixels(element)) {
4787            vl = 0xffffffff;
4788        }
4789        // VL
4790        if (is32bitVLVR) {
4791            byteOffset = this.writeUint32(byteOffset, vl);
4792        } else {
4793            byteOffset = this.writeUint16(byteOffset, vl);
4794        }
4795
4796        // value
4797        var value = element.value;
4798        // check value
4799        if (typeof value === 'undefined') {
4800            value = [];
4801        }
4802        // write
4803        if (element.tag.name === "x7FE00010") {
4804            byteOffset = this.writePixelDataElementValue(element.vr, element.vl, byteOffset, value, isImplicit);
4805        } else {
4806            byteOffset = this.writeDataElementValue(element.vr, byteOffset, value, isImplicit);
4807        }
4808
4809        // sequence delimitation item for sequence with implicit length
4810        if (dwv.dicom.isImplicitLengthSequence(element) ||
4811            dwv.dicom.isImplicitLengthPixels(element)) {
4812            var seqDelimElement = {
4813                'tag': {
4814                    group: "0xFFFE",
4815                    element: "0xE0DD",
4816                    name: "SequenceDelimitationItem"
4817                },
4818                'vr': "NONE",
4819                'vl': 0,
4820                'value': []
4821            };
4822            byteOffset = this.writeDataElement(seqDelimElement, byteOffset, isImplicit);
4823        }
4824
4825        // return new offset
4826        return byteOffset;
4827    };
4828
4829    /**
4830     * Is this element an implicit length sequence?
4831     * @param {Object} element The element to check.
4832     * @returns {Boolean} True if it is.
4833     */
4834    dwv.dicom.isImplicitLengthSequence = function(element) {
4835        // sequence with no length
4836        return (element.vr === "SQ") &&
4837            (element.vl === "u/l");
4838    };
4839
4840    /**
4841     * Is this element an implicit length item?
4842     * @param {Object} element The element to check.
4843     * @returns {Boolean} True if it is.
4844     */
4845    dwv.dicom.isImplicitLengthItem = function(element) {
4846        // item with no length
4847        return (element.tag.name === "xFFFEE000") &&
4848            (element.vl === "u/l");
4849    };
4850
4851    /**
4852     * Is this element an implicit length pixel data?
4853     * @param {Object} element The element to check.
4854     * @returns {Boolean} True if it is.
4855     */
4856    dwv.dicom.isImplicitLengthPixels = function(element) {
4857        // pixel data with no length
4858        return (element.tag.name === "x7FE00010") &&
4859            (element.vl === "u/l");
4860    };
4861
4862    /**
4863     * Helper method to flatten an array of typed arrays to 2D typed array
4864     * @param {Array} array of typed arrays
4865     * @returns {Object} a typed array containing all values
4866     */
4867    dwv.dicom.flattenArrayOfTypedArrays = function(initialArray) {
4868        var initialArrayLength = initialArray.length;
4869        var arrayLength = initialArray[0].length;
4870        var flattenendArrayLength = initialArrayLength * arrayLength;
4871
4872        var flattenedArray = new initialArray[0].constructor(flattenendArrayLength);
4873
4874        for (var i = 0; i < initialArrayLength; i++) {
4875            var indexFlattenedArray = i * arrayLength;
4876            flattenedArray.set(initialArray[i], indexFlattenedArray);
4877        }
4878        return flattenedArray;
4879    };
4880
4881    /**
4882     * DICOM writer.
4883     * @constructor
4884     */
4885    dwv.dicom.DicomWriter = function() {
4886
4887        // possible tag actions
4888        var actions = {
4889            'copy': function(item) { return item; },
4890            'remove': function() { return null; },
4891            'clear': function(item) {
4892                item.value[0] = "";
4893                item.vl = 0;
4894                item.endOffset = item.startOffset;
4895                return item;
4896            },
4897            'replace': function(item, value) {
4898                item.value[0] = value;
4899                item.vl = value.length;
4900                item.endOffset = item.startOffset + value.length;
4901                return item;
4902            }
4903        };
4904
4905        // default rules: just copy
4906        var defaultRules = {
4907            'default': { action: 'copy', value: null }
4908        };
4909
4910        /**
4911         * Public (modifiable) rules.
4912         * Set of objects as:
4913         *   name : { action: 'actionName', value: 'optionalValue }
4914         * The names are either 'default', tagName or groupName.
4915         * Each DICOM element will be checked to see if a rule is applicable.
4916         * First checked by tagName and then by groupName,
4917         * if nothing is found the default rule is applied.
4918         */
4919        this.rules = defaultRules;
4920
4921        /**
4922         * Example anonymisation rules.
4923         */
4924        this.anonymisationRules = {
4925            'default': { action: 'remove', value: null },
4926            'PatientName': { action: 'replace', value: 'Anonymized' }, // tag
4927            'Meta Element': { action: 'copy', value: null }, // group 'x0002'
4928            'Acquisition': { action: 'copy', value: null }, // group 'x0018'
4929            'Image Presentation': { action: 'copy', value: null }, // group 'x0028'
4930            'Procedure': { action: 'copy', value: null }, // group 'x0040'
4931            'Pixel Data': { action: 'copy', value: null } // group 'x7fe0'
4932        };
4933
4934        /**
4935         * Get the element to write according to the class rules.
4936         * Priority order: tagName, groupName, default.
4937         * @param {Object} element The element to check
4938         * @return {Object} The element to write, can be null.
4939         */
4940        this.getElementToWrite = function(element) {
4941            // get group and tag string name
4942            var tagName = null;
4943            var dict = dwv.dicom.dictionary;
4944            var group = element.tag.group;
4945            var groupName = dwv.dicom.TagGroups[group.substr(1)]; // remove first 0
4946
4947            if (typeof dict[group] !== 'undefined' && typeof dict[group][element.tag.element] !== 'undefined') {
4948                tagName = dict[group][element.tag.element][2];
4949            }
4950            // apply rules:
4951            var rule;
4952            // 1. tag itself
4953            if (typeof this.rules[element.tag.name] !== 'undefined') {
4954                rule = this.rules[element.tag.name];
4955            }
4956            // 2. tag name
4957            else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') {
4958                rule = this.rules[tagName];
4959            }
4960            // 3. group name
4961            else if (typeof this.rules[groupName] !== 'undefined') {
4962                rule = this.rules[groupName];
4963            }
4964            // 4. default
4965            else {
4966                rule = this.rules['default'];
4967            }
4968            // apply action on element and return
4969            return actions[rule.action](element, rule.value);
4970        };
4971    };
4972
4973    /**
4974     * Get the ArrayBuffer corresponding to input DICOM elements.
4975     * @param {Array} dicomElements The wrapped elements to write.
4976     * @returns {ArrayBuffer} The elements as a buffer.
4977     */
4978    dwv.dicom.DicomWriter.prototype.getBuffer = function(dicomElements) {
4979        // array keys
4980        var keys = Object.keys(dicomElements);
4981
4982        // transfer syntax
4983        var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]);
4984        var isImplicit = dwv.dicom.isImplicitTransferSyntax(syntax);
4985        var isBigEndian = dwv.dicom.isBigEndianTransferSyntax(syntax);
4986
4987        // calculate buffer size and split elements (meta and non meta)
4988        var totalSize = 128 + 4; // DICM
4989        var localSize = 0;
4990        var metaElements = [];
4991        var rawElements = [];
4992        var element;
4993        var groupName;
4994        var metaLength = 0;
4995        var fmiglTag = dwv.dicom.getFileMetaInformationGroupLengthTag();
4996        var icUIDTag = new dwv.dicom.Tag("0x0002", "0x0012"); // ImplementationClassUID
4997        var ivnTag = new dwv.dicom.Tag("0x0002", "0x0013"); // ImplementationVersionName
4998        for (var i = 0, leni = keys.length; i < leni; ++i) {
4999            element = this.getElementToWrite(dicomElements[keys[i]]);
5000            if (element !== null &&
5001                !fmiglTag.equals2(element.tag) &&
5002                !icUIDTag.equals2(element.tag) &&
5003                !ivnTag.equals2(element.tag)) {
5004                localSize = 0;
5005                // tag group name
5006                groupName = dwv.dicom.TagGroups[element.tag.group.substr(1)]; // remove first 0
5007
5008                // prefix
5009                if (groupName === 'Meta Element') {
5010                    localSize += dwv.dicom.getDataElementPrefixByteSize(element.vr, false);
5011                } else {
5012                    localSize += dwv.dicom.getDataElementPrefixByteSize(element.vr, isImplicit);
5013                }
5014
5015                // value
5016                var realVl = element.endOffset - element.startOffset;
5017                localSize += parseInt(realVl, 10);
5018
5019                // add size of sequence delimitation item
5020                if (dwv.dicom.isImplicitLengthSequence(element) ||
5021                    dwv.dicom.isImplicitLengthPixels(element)) {
5022                    localSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit);
5023                }
5024
5025                // sort elements
5026                if (groupName === 'Meta Element') {
5027                    metaElements.push(element);
5028                    metaLength += localSize;
5029                } else {
5030                    rawElements.push(element);
5031                }
5032
5033                // add to total size
5034                totalSize += localSize;
5035            }
5036        }
5037
5038        // ImplementationClassUID
5039        var icUID = dwv.dicom.getDicomElement("ImplementationClassUID");
5040        var icUIDSize = dwv.dicom.getDataElementPrefixByteSize(icUID.vr, isImplicit);
5041        icUIDSize += dwv.dicom.setElementValue(icUID, "1.2.826.0.1.3680043.9.7278.1." + dwv.getVersion(), false);
5042        metaElements.push(icUID);
5043        metaLength += icUIDSize;
5044        totalSize += icUIDSize;
5045        // ImplementationVersionName
5046        var ivn = dwv.dicom.getDicomElement("ImplementationVersionName");
5047        var ivnSize = dwv.dicom.getDataElementPrefixByteSize(ivn.vr, isImplicit);
5048        ivnSize += dwv.dicom.setElementValue(ivn, "DWV_" + dwv.getVersion(), false);
5049        metaElements.push(ivn);
5050        metaLength += ivnSize;
5051        totalSize += ivnSize;
5052
5053        // create the FileMetaInformationGroupLength element
5054        var fmigl = dwv.dicom.getDicomElement("FileMetaInformationGroupLength");
5055        var fmiglSize = dwv.dicom.getDataElementPrefixByteSize(fmigl.vr, isImplicit);
5056        fmiglSize += dwv.dicom.setElementValue(fmigl, metaLength, false);
5057
5058        // add its size to the total one
5059        totalSize += fmiglSize;
5060
5061        // create buffer
5062        var buffer = new ArrayBuffer(totalSize);
5063        var metaWriter = new dwv.dicom.DataWriter(buffer);
5064        var dataWriter = new dwv.dicom.DataWriter(buffer, !isBigEndian);
5065        var offset = 128;
5066        // DICM
5067        offset = metaWriter.writeString(offset, "DICM");
5068        // FileMetaInformationGroupLength
5069        offset = metaWriter.writeDataElement(fmigl, offset, false);
5070        // write meta
5071        for (var j = 0, lenj = metaElements.length; j < lenj; ++j) {
5072            offset = metaWriter.writeDataElement(metaElements[j], offset, false);
5073        }
5074        // write non meta
5075        for (var k = 0, lenk = rawElements.length; k < lenk; ++k) {
5076            offset = dataWriter.writeDataElement(rawElements[k], offset, isImplicit);
5077        }
5078
5079        // return
5080        return buffer;
5081    };
5082
5083    /**
5084     * Get a DICOM element from its tag name (value set separatly).
5085     * @param {String} tagName The string tag name.
5086     * @return {Object} The DICOM element.
5087     */
5088    dwv.dicom.getDicomElement = function(tagName) {
5089        var tagGE = dwv.dicom.getGroupElementFromName(tagName);
5090        var dict = dwv.dicom.dictionary;
5091        // return element definition
5092        return {
5093            'tag': { 'group': tagGE.group, 'element': tagGE.element },
5094            'vr': dict[tagGE.group][tagGE.element][0],
5095            'vl': dict[tagGE.group][tagGE.element][1]
5096        };
5097    };
5098
5099    /**
5100     * Set a DICOM element value according to its VR (Value Representation).
5101     * @param {Object} element The DICOM element to set the value.
5102     * @param {Object} value The value to set.
5103     * @param {Boolean} isImplicit Does the data use implicit VR?
5104     * @return {Number} The total element size.
5105     */
5106    dwv.dicom.setElementValue = function(element, value, isImplicit) {
5107        // byte size of the element
5108        var size = 0;
5109        // special sequence case
5110        if (element.vr === "SQ") {
5111
5112            // set the value
5113            element.value = value;
5114            element.vl = 0;
5115
5116            if (value !== null && value !== 0) {
5117                var sqItems = [];
5118                var name;
5119
5120                // explicit or implicit length
5121                var explicitLength = true;
5122                if (typeof value.explicitLength !== "undefined") {
5123                    explicitLength = value.explicitLength;
5124                    delete value.explicitLength;
5125                }
5126
5127                // items
5128                var itemData;
5129                var itemKeys = Object.keys(value);
5130                for (var i = 0, leni = itemKeys.length; i < leni; ++i) {
5131                    var itemElements = {};
5132                    var subSize = 0;
5133                    itemData = value[itemKeys[i]];
5134
5135                    // check data
5136                    if (itemData === null || itemData === 0) {
5137                        continue;
5138                    }
5139
5140                    // elements
5141                    var subElement;
5142                    var elemKeys = Object.keys(itemData);
5143                    for (var j = 0, lenj = elemKeys.length; j < lenj; ++j) {
5144                        subElement = dwv.dicom.getDicomElement(elemKeys[j]);
5145                        subSize += dwv.dicom.setElementValue(subElement, itemData[elemKeys[j]]);
5146
5147                        // add sequence delimitation size for sub sequences
5148                        if (dwv.dicom.isImplicitLengthSequence(subElement)) {
5149                            subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit);
5150                        }
5151
5152                        name = dwv.dicom.getGroupElementKey(subElement.tag.group, subElement.tag.element);
5153                        itemElements[name] = subElement;
5154                        subSize += dwv.dicom.getDataElementPrefixByteSize(subElement.vr, isImplicit);
5155                    }
5156
5157                    // item (after elements to get the size)
5158                    var itemElement = {
5159                        'tag': { 'group': "0xFFFE", 'element': "0xE000" },
5160                        'vr': "NONE",
5161                        'vl': (explicitLength ? subSize : "u/l"),
5162                        'value': []
5163                    };
5164                    name = dwv.dicom.getGroupElementKey(itemElement.tag.group, itemElement.tag.element);
5165                    itemElements[name] = itemElement;
5166                    subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit);
5167
5168                    // item delimitation
5169                    if (!explicitLength) {
5170                        var itemDelimElement = {
5171                            'tag': { 'group': "0xFFFE", 'element': "0xE00D" },
5172                            'vr': "NONE",
5173                            'vl': 0,
5174                            'value': []
5175                        };
5176                        name = dwv.dicom.getGroupElementKey(itemDelimElement.tag.group, itemDelimElement.tag.element);
5177                        itemElements[name] = itemDelimElement;
5178                        subSize += dwv.dicom.getDataElementPrefixByteSize("NONE", isImplicit);
5179                    }
5180
5181                    size += subSize;
5182                    sqItems.push(itemElements);
5183                }
5184
5185                element.value = sqItems;
5186                if (explicitLength) {
5187                    element.vl = size;
5188                } else {
5189                    element.vl = "u/l";
5190                }
5191            }
5192        } else {
5193            // set the value and calculate size
5194            size = 0;
5195            if (value instanceof Array) {
5196                element.value = value;
5197                for (var k = 0; k < value.length; ++k) {
5198                    // spearator
5199                    if (k !== 0) {
5200                        size += 1;
5201                    }
5202                    // value
5203                    size += value[k].toString().length;
5204                }
5205            } else {
5206                element.value = [value];
5207                if (typeof value !== "undefined" && typeof value.length !== "undefined") {
5208                    size = value.length;
5209                } else {
5210                    // numbers
5211                    size = 1;
5212                }
5213            }
5214
5215            // convert size to bytes
5216            if (element.vr === "US" || element.vr === "OW") {
5217                size *= Uint16Array.BYTES_PER_ELEMENT;
5218            } else if (element.vr === "SS") {
5219                size *= Int16Array.BYTES_PER_ELEMENT;
5220            } else if (element.vr === "UL") {
5221                size *= Uint32Array.BYTES_PER_ELEMENT;
5222            } else if (element.vr === "SL") {
5223                size *= Int32Array.BYTES_PER_ELEMENT;
5224            } else if (element.vr === "FL") {
5225                size *= Float32Array.BYTES_PER_ELEMENT;
5226            } else if (element.vr === "FD") {
5227                size *= Float64Array.BYTES_PER_ELEMENT;
5228            } else {
5229                size *= Uint8Array.BYTES_PER_ELEMENT;
5230            }
5231            element.vl = size;
5232        }
5233
5234        // return the size of that data
5235        return size;
5236    };
5237
5238    /**
5239     * Get the DICOM element from a DICOM tags object.
5240     * @param {Object} tags The DICOM tags object.
5241     * @return {Object} The DICOM elements and the end offset.
5242     */
5243    dwv.dicom.getElementsFromJSONTags = function(tags) {
5244        // transfer syntax
5245        var isImplicit = dwv.dicom.isImplicitTransferSyntax(tags.TransferSyntaxUID);
5246        // convert JSON to DICOM element object
5247        var keys = Object.keys(tags);
5248        var dicomElements = {};
5249        var dicomElement;
5250        var name;
5251        var offset = 128 + 4; // preamble
5252        var size;
5253        for (var k = 0, len = keys.length; k < len; ++k) {
5254            // get the DICOM element definition from its name
5255            dicomElement = dwv.dicom.getDicomElement(keys[k]);
5256            // set its value
5257            size = dwv.dicom.setElementValue(dicomElement, tags[keys[k]], isImplicit);
5258            // set offsets
5259            offset += dwv.dicom.getDataElementPrefixByteSize(dicomElement.vr, isImplicit);
5260            dicomElement.startOffset = offset;
5261            offset += size;
5262            dicomElement.endOffset = offset;
5263            // create the tag group/element key
5264            name = dwv.dicom.getGroupElementKey(dicomElement.tag.group, dicomElement.tag.element);
5265            // store
5266            dicomElements[name] = dicomElement;
5267        }
5268        // return
5269        return { 'elements': dicomElements, 'offset': offset };
5270    };
5271
5272    /**
5273     * GradSquarePixGenerator
5274     * Generates a small gradient square.
5275     * @param {Number} numberOfColumns The image number of columns.
5276     * @param {Number} numberOfRows The image number of rows.
5277     * @constructor
5278     */
5279    var GradSquarePixGenerator = function(numberOfColumns, numberOfRows) {
5280
5281        var halfCols = numberOfColumns * 0.5;
5282        var halfRows = numberOfRows * 0.5;
5283        var maxNoBounds = (numberOfColumns / 2 + halfCols / 2) * (numberOfRows / 2 + halfRows / 2);
5284        var max = 100;
5285
5286        /**
5287         * Get a grey value.
5288         * @param {Number} i The column index.
5289         * @param {Number} j The row index.
5290         * @return {Array} The grey value.
5291         */
5292        this.getGrey = function(i, j) {
5293            var value = max;
5294            var jc = Math.abs(j - halfRows);
5295            var ic = Math.abs(i - halfCols);
5296            if (jc < halfRows / 2 && ic < halfCols / 2) {
5297                value += (i * j) * (max / maxNoBounds);
5298            }
5299            return [value];
5300        };
5301
5302        /**
5303         * Get RGB values.
5304         * @param {Number} i The column index.
5305         * @param {Number} j The row index.
5306         * @return {Array} The [R,G,B] values.
5307         */
5308        this.getRGB = function(i, j) {
5309            var value = 0;
5310            var jc = Math.abs(j - halfRows);
5311            var ic = Math.abs(i - halfCols);
5312            if (jc < halfRows / 2 && ic < halfCols / 2) {
5313                value += (i * j) * (max / maxNoBounds);
5314            }
5315            if (value > 255) {
5316                value = 200;
5317            }
5318            return [0, value, value];
5319        };
5320    };
5321
5322    // List of pixel generators.
5323    dwv.dicom.pixelGenerators = {
5324        'gradSquare': GradSquarePixGenerator
5325    };
5326
5327    /**
5328     * Get the DICOM pixel data from a DICOM tags object.
5329     * @param {Object} tags The DICOM tags object.
5330     * @param {Object} startOffset The start offset of the pixel data.
5331     * @param {String} pixGeneratorName The name of a pixel generator.
5332     * @return {Object} The DICOM pixel data element.
5333     */
5334    dwv.dicom.generatePixelDataFromJSONTags = function(tags, startOffset, pixGeneratorName) {
5335
5336        // default generator
5337        if (typeof pixGeneratorName === "undefined") {
5338            pixGeneratorName = "gradSquare";
5339        }
5340
5341        // check tags
5342        if (typeof tags.TransferSyntaxUID === "undefined") {
5343            throw new Error("Missing transfer syntax for pixel generation.");
5344        } else if (typeof tags.Rows === "undefined") {
5345            throw new Error("Missing number of rows for pixel generation.");
5346        } else if (typeof tags.Columns === "undefined") {
5347            throw new Error("Missing number of columns for pixel generation.");
5348        } else if (typeof tags.BitsAllocated === "undefined") {
5349            throw new Error("Missing BitsAllocated for pixel generation.");
5350        } else if (typeof tags.PixelRepresentation === "undefined") {
5351            throw new Error("Missing PixelRepresentation for pixel generation.");
5352        } else if (typeof tags.SamplesPerPixel === "undefined") {
5353            throw new Error("Missing SamplesPerPixel for pixel generation.");
5354        } else if (typeof tags.PhotometricInterpretation === "undefined") {
5355            throw new Error("Missing PhotometricInterpretation for pixel generation.");
5356        }
5357
5358        // extract info from tags
5359        var isImplicit = dwv.dicom.isImplicitTransferSyntax(tags.TransferSyntaxUID);
5360        var numberOfRows = tags.Rows;
5361        var numberOfColumns = tags.Columns;
5362        var bitsAllocated = tags.BitsAllocated;
5363        var pixelRepresentation = tags.PixelRepresentation;
5364        var samplesPerPixel = tags.SamplesPerPixel;
5365        var photometricInterpretation = tags.PhotometricInterpretation;
5366
5367        var sliceLength = numberOfRows * numberOfColumns;
5368        var dataLength = sliceLength * samplesPerPixel;
5369
5370        // check values
5371        if (samplesPerPixel !== 1 && samplesPerPixel !== 3) {
5372            throw new Error("Unsupported SamplesPerPixel for pixel generation: " + samplesPerPixel);
5373        }
5374        if ((samplesPerPixel === 1 && !(photometricInterpretation === "MONOCHROME1" ||
5375                photometricInterpretation === "MONOCHROME2")) ||
5376            (samplesPerPixel === 3 && photometricInterpretation !== "RGB")) {
5377            throw new Error("Unsupported PhotometricInterpretation for pixel generation: " +
5378                photometricInterpretation + " with SamplesPerPixel: " + samplesPerPixel);
5379        }
5380
5381        var nSamples = 1;
5382        var nColourPlanes = 1;
5383        if (samplesPerPixel === 3) {
5384            if (typeof tags.PlanarConfiguration === "undefined") {
5385                throw new Error("Missing PlanarConfiguration for pixel generation.");
5386            }
5387            var planarConfiguration = tags.PlanarConfiguration;
5388            if (planarConfiguration !== 0 && planarConfiguration !== 1) {
5389                throw new Error("Unsupported PlanarConfiguration for pixel generation: " + planarConfiguration);
5390            }
5391            if (planarConfiguration === 0) {
5392                nSamples = 3;
5393            } else {
5394                nColourPlanes = 3;
5395            }
5396        }
5397
5398        // create pixel array
5399        var pixels = dwv.dicom.getTypedArray(
5400            bitsAllocated, pixelRepresentation, dataLength);
5401
5402        // pixels generator
5403        if (typeof dwv.dicom.pixelGenerators[pixGeneratorName] === "undefined") {
5404            throw new Error("Unknown PixelData generator: " + pixGeneratorName);
5405        }
5406        var generator = new dwv.dicom.pixelGenerators[pixGeneratorName](numberOfColumns, numberOfRows);
5407        var generate = generator.getGrey;
5408        if (photometricInterpretation === "RGB") {
5409            generate = generator.getRGB;
5410        }
5411
5412        // main loop
5413        var offset = 0;
5414        for (var c = 0; c < nColourPlanes; ++c) {
5415            for (var j = 0; j < numberOfRows; ++j) {
5416                for (var i = 0; i < numberOfColumns; ++i) {
5417                    for (var s = 0; s < nSamples; ++s) {
5418                        if (nColourPlanes !== 1) {
5419                            pixels[offset] = generate(i, j)[c];
5420                        } else {
5421                            pixels[offset] = generate(i, j)[s];
5422                        }
5423                        ++offset;
5424                    }
5425                }
5426            }
5427        }
5428
5429        // create and return the DICOM element
5430        var vr = "OW";
5431        if (bitsAllocated === 8) {
5432            vr = "OB";
5433        }
5434        var pixVL = dwv.dicom.getDataElementPrefixByteSize(vr, isImplicit) +
5435            (pixels.BYTES_PER_ELEMENT * dataLength);
5436        return {
5437            'tag': { 'group': "0x7FE0", 'element': "0x0010" },
5438            'vr': vr,
5439            'vl': pixVL,
5440            'value': pixels,
5441            'startOffset': startOffset,
5442            'endOffset': startOffset + pixVL
5443        };
5444    };
5445
5446    // namespaces
5447    var dwv = dwv || {};
5448    dwv.dicom = dwv.dicom || {};
5449
5450    /**
5451     * DICOM tag dictionary.
5452     * Generated using xml standard conversion
5453     *  from {@link https://github.com/ivmartel/dcmbench/tree/master/view/part06}
5454     *  with {@link http://medical.nema.org/medical/dicom/current/source/docbook/part06/part06.xml}
5455     * Conversion changes:
5456     * - (vr) "See Note" -> "NONE", "OB or OW" -> "ox", "US or SS" -> "xs"
5457     * - added "GenericGroupLength" element to each group
5458     * Local changes:
5459     * - tag numbers with 'xx' were replaced with '00', 'xxx' with '001' and 'xxxx' with '0004'
5460     */
5461    dwv.dicom.dictionary = {
5462        '0x0000': {
5463            '0x0000': ['UL', '1', 'GroupLength'],
5464            '0x0001': ['UL', '1', 'CommandLengthToEnd'],
5465            '0x0002': ['UI', '1', 'AffectedSOPClassUID'],
5466            '0x0003': ['UI', '1', 'RequestedSOPClassUID'],
5467            '0x0010': ['CS', '1', 'CommandRecognitionCode'],
5468            '0x0100': ['US', '1', 'CommandField'],
5469            '0x0110': ['US', '1', 'MessageID'],
5470            '0x0120': ['US', '1', 'MessageIDBeingRespondedTo'],
5471            '0x0200': ['AE', '1', 'Initiator'],
5472            '0x0300': ['AE', '1', 'Receiver'],
5473            '0x0400': ['AE', '1', 'FindLocation'],
5474            '0x0600': ['AE', '1', 'MoveDestination'],
5475            '0x0700': ['US', '1', 'Priority'],
5476            '0x0800': ['US', '1', 'DataSetType'],
5477            '0x0850': ['US', '1', 'NumberOfMatches'],
5478            '0x0860': ['US', '1', 'ResponseSequenceNumber'],
5479            '0x0900': ['US', '1', 'Status'],
5480            '0x0901': ['AT', '1-n', 'OffendingElement'],
5481            '0x0902': ['LO', '1', 'ErrorComment'],
5482            '0x0903': ['US', '1', 'ErrorID'],
5483            '0x0904': ['OT', '1-n', 'ErrorInformation'],
5484            '0x1000': ['UI', '1', 'AffectedSOPInstanceUID'],
5485            '0x1001': ['UI', '1', 'RequestedSOPInstanceUID'],
5486            '0x1002': ['US', '1', 'EventTypeID'],
5487            '0x1003': ['OT', '1-n', 'EventInformation'],
5488            '0x1005': ['AT', '1-n', 'AttributeIdentifierList'],
5489            '0x1007': ['AT', '1-n', 'ModificationList'],
5490            '0x1008': ['US', '1', 'ActionTypeID'],
5491            '0x1009': ['OT', '1-n', 'ActionInformation'],
5492            '0x1013': ['UI', '1-n', 'SuccessfulSOPInstanceUIDList'],
5493            '0x1014': ['UI', '1-n', 'FailedSOPInstanceUIDList'],
5494            '0x1015': ['UI', '1-n', 'WarningSOPInstanceUIDList'],
5495            '0x1020': ['US', '1', 'NumberOfRemainingSuboperations'],
5496            '0x1021': ['US', '1', 'NumberOfCompletedSuboperations'],
5497            '0x1022': ['US', '1', 'NumberOfFailedSuboperations'],
5498            '0x1023': ['US', '1', 'NumberOfWarningSuboperations'],
5499            '0x1030': ['AE', '1', 'MoveOriginatorApplicationEntityTitle'],
5500            '0x1031': ['US', '1', 'MoveOriginatorMessageID'],
5501            '0x4000': ['AT', '1', 'DialogReceiver'],
5502            '0x4010': ['AT', '1', 'TerminalType'],
5503            '0x5010': ['SH', '1', 'MessageSetID'],
5504            '0x5020': ['SH', '1', 'EndMessageSet'],
5505            '0x5110': ['AT', '1', 'DisplayFormat'],
5506            '0x5120': ['AT', '1', 'PagePositionID'],
5507            '0x5130': ['CS', '1', 'TextFormatID'],
5508            '0x5140': ['CS', '1', 'NormalReverse'],
5509            '0x5150': ['CS', '1', 'AddGrayScale'],
5510            '0x5160': ['CS', '1', 'Borders'],
5511            '0x5170': ['IS', '1', 'Copies'],
5512            '0x5180': ['CS', '1', 'OldMagnificationType'],
5513            '0x5190': ['CS', '1', 'Erase'],
5514            '0x51A0': ['CS', '1', 'Print'],
5515            '0x51B0': ['US', '1-n', 'Overlays']
5516        },
5517        '0x0002': {
5518            '0x0000': ['UL', '1', 'FileMetaInformationGroupLength'],
5519            '0x0001': ['OB', '1', 'FileMetaInformationVersion'],
5520            '0x0002': ['UI', '1', 'MediaStorageSOPClassUID'],
5521            '0x0003': ['UI', '1', 'MediaStorageSOPInstanceUID'],
5522            '0x0010': ['UI', '1', 'TransferSyntaxUID'],
5523            '0x0012': ['UI', '1', 'ImplementationClassUID'],
5524            '0x0013': ['SH', '1', 'ImplementationVersionName'],
5525            '0x0016': ['AE', '1', 'SourceApplicationEntityTitle'],
5526            '0x0017': ['AE', '1', 'SendingApplicationEntityTitle'],
5527            '0x0018': ['AE', '1', 'ReceivingApplicationEntityTitle'],
5528            '0x0100': ['UI', '1', 'PrivateInformationCreatorUID'],
5529            '0x0102': ['OB', '1', 'PrivateInformation']
5530        },
5531        '0x0004': {
5532            '0x0000': ['UL', '1', 'GenericGroupLength'],
5533            '0x1130': ['CS', '1', 'FileSetID'],
5534            '0x1141': ['CS', '1-8', 'FileSetDescriptorFileID'],
5535            '0x1142': ['CS', '1', 'SpecificCharacterSetOfFileSetDescriptorFile'],
5536            '0x1200': ['UL', '1', 'OffsetOfTheFirstDirectoryRecordOfTheRootDirectoryEntity'],
5537            '0x1202': ['UL', '1', 'OffsetOfTheLastDirectoryRecordOfTheRootDirectoryEntity'],
5538            '0x1212': ['US', '1', 'FileSetConsistencyFlag'],
5539            '0x1220': ['SQ', '1', 'DirectoryRecordSequence'],
5540            '0x1400': ['UL', '1', 'OffsetOfTheNextDirectoryRecord'],
5541            '0x1410': ['US', '1', 'RecordInUseFlag'],
5542            '0x1420': ['UL', '1', 'OffsetOfReferencedLowerLevelDirectoryEntity'],
5543            '0x1430': ['CS', '1', 'DirectoryRecordType'],
5544            '0x1432': ['UI', '1', 'PrivateRecordUID'],
5545            '0x1500': ['CS', '1-8', 'ReferencedFileID'],
5546            '0x1504': ['UL', '1', 'MRDRDirectoryRecordOffset'],
5547            '0x1510': ['UI', '1', 'ReferencedSOPClassUIDInFile'],
5548            '0x1511': ['UI', '1', 'ReferencedSOPInstanceUIDInFile'],
5549            '0x1512': ['UI', '1', 'ReferencedTransferSyntaxUIDInFile'],
5550            '0x151A': ['UI', '1-n', 'ReferencedRelatedGeneralSOPClassUIDInFile'],
5551            '0x1600': ['UL', '1', 'NumberOfReferences']
5552        },
5553        '0x0008': {
5554            '0x0000': ['UL', '1', 'GenericGroupLength'],
5555            '0x0001': ['UL', '1', 'LengthToEnd'],
5556            '0x0005': ['CS', '1-n', 'SpecificCharacterSet'],
5557            '0x0006': ['SQ', '1', 'LanguageCodeSequence'],
5558            '0x0008': ['CS', '2-n', 'ImageType'],
5559            '0x0010': ['SH', '1', 'RecognitionCode'],
5560            '0x0012': ['DA', '1', 'InstanceCreationDate'],
5561            '0x0013': ['TM', '1', 'InstanceCreationTime'],
5562            '0x0014': ['UI', '1', 'InstanceCreatorUID'],
5563            '0x0015': ['DT', '1', 'InstanceCoercionDateTime'],
5564            '0x0016': ['UI', '1', 'SOPClassUID'],
5565            '0x0018': ['UI', '1', 'SOPInstanceUID'],
5566            '0x001A': ['UI', '1-n', 'RelatedGeneralSOPClassUID'],
5567            '0x001B': ['UI', '1', 'OriginalSpecializedSOPClassUID'],
5568            '0x0020': ['DA', '1', 'StudyDate'],
5569            '0x0021': ['DA', '1', 'SeriesDate'],
5570            '0x0022': ['DA', '1', 'AcquisitionDate'],
5571            '0x0023': ['DA', '1', 'ContentDate'],
5572            '0x0024': ['DA', '1', 'OverlayDate'],
5573            '0x0025': ['DA', '1', 'CurveDate'],
5574            '0x002A': ['DT', '1', 'AcquisitionDateTime'],
5575            '0x0030': ['TM', '1', 'StudyTime'],
5576            '0x0031': ['TM', '1', 'SeriesTime'],
5577            '0x0032': ['TM', '1', 'AcquisitionTime'],
5578            '0x0033': ['TM', '1', 'ContentTime'],
5579            '0x0034': ['TM', '1', 'OverlayTime'],
5580            '0x0035': ['TM', '1', 'CurveTime'],
5581            '0x0040': ['US', '1', 'DataSetType'],
5582            '0x0041': ['LO', '1', 'DataSetSubtype'],
5583            '0x0042': ['CS', '1', 'NuclearMedicineSeriesType'],
5584            '0x0050': ['SH', '1', 'AccessionNumber'],
5585            '0x0051': ['SQ', '1', 'IssuerOfAccessionNumberSequence'],
5586            '0x0052': ['CS', '1', 'QueryRetrieveLevel'],
5587            '0x0053': ['CS', '1', 'QueryRetrieveView'],
5588            '0x0054': ['AE', '1-n', 'RetrieveAETitle'],
5589            '0x0056': ['CS', '1', 'InstanceAvailability'],
5590            '0x0058': ['UI', '1-n', 'FailedSOPInstanceUIDList'],
5591            '0x0060': ['CS', '1', 'Modality'],
5592            '0x0061': ['CS', '1-n', 'ModalitiesInStudy'],
5593            '0x0062': ['UI', '1-n', 'SOPClassesInStudy'],
5594            '0x0064': ['CS', '1', 'ConversionType'],
5595            '0x0068': ['CS', '1', 'PresentationIntentType'],
5596            '0x0070': ['LO', '1', 'Manufacturer'],
5597            '0x0080': ['LO', '1', 'InstitutionName'],
5598            '0x0081': ['ST', '1', 'InstitutionAddress'],
5599            '0x0082': ['SQ', '1', 'InstitutionCodeSequence'],
5600            '0x0090': ['PN', '1', 'ReferringPhysicianName'],
5601            '0x0092': ['ST', '1', 'ReferringPhysicianAddress'],
5602            '0x0094': ['SH', '1-n', 'ReferringPhysicianTelephoneNumbers'],
5603            '0x0096': ['SQ', '1', 'ReferringPhysicianIdentificationSequence'],
5604            '0x009C': ['PN', '1-n', 'ConsultingPhysicianName'],
5605            '0x009D': ['SQ', '1', 'ConsultingPhysicianIdentificationSequence'],
5606            '0x0100': ['SH', '1', 'CodeValue'],
5607            '0x0101': ['LO', '1', 'ExtendedCodeValue'],
5608            '0x0102': ['SH', '1', 'CodingSchemeDesignator'],
5609            '0x0103': ['SH', '1', 'CodingSchemeVersion'],
5610            '0x0104': ['LO', '1', 'CodeMeaning'],
5611            '0x0105': ['CS', '1', 'MappingResource'],
5612            '0x0106': ['DT', '1', 'ContextGroupVersion'],
5613            '0x0107': ['DT', '1', 'ContextGroupLocalVersion'],
5614            '0x0108': ['LT', '1', 'ExtendedCodeMeaning'],
5615            '0x010B': ['CS', '1', 'ContextGroupExtensionFlag'],
5616            '0x010C': ['UI', '1', 'CodingSchemeUID'],
5617            '0x010D': ['UI', '1', 'ContextGroupExtensionCreatorUID'],
5618            '0x010F': ['CS', '1', 'ContextIdentifier'],
5619            '0x0110': ['SQ', '1', 'CodingSchemeIdentificationSequence'],
5620            '0x0112': ['LO', '1', 'CodingSchemeRegistry'],
5621            '0x0114': ['ST', '1', 'CodingSchemeExternalID'],
5622            '0x0115': ['ST', '1', 'CodingSchemeName'],
5623            '0x0116': ['ST', '1', 'CodingSchemeResponsibleOrganization'],
5624            '0x0117': ['UI', '1', 'ContextUID'],
5625            '0x0118': ['UI', '1', 'MappingResourceUID'],
5626            '0x0119': ['UC', '1', 'LongCodeValue'],
5627            '0x0120': ['UR', '1', 'URNCodeValue'],
5628            '0x0121': ['SQ', '1', 'EquivalentCodeSequence'],
5629            '0x0201': ['SH', '1', 'TimezoneOffsetFromUTC'],
5630            '0x0300': ['SQ', '1', 'PrivateDataElementCharacteristicsSequence'],
5631            '0x0301': ['US', '1', 'PrivateGroupReference'],
5632            '0x0302': ['LO', '1', 'PrivateCreatorReference'],
5633            '0x0303': ['CS', '1', 'BlockIdentifyingInformationStatus'],
5634            '0x0304': ['US', '1-n', 'NonidentifyingPrivateElements'],
5635            '0x0306': ['US', '1-n', 'IdentifyingPrivateElements'],
5636            '0x0305': ['SQ', '1', 'DeidentificationActionSequence'],
5637            '0x0307': ['CS', '1', 'DeidentificationAction'],
5638            '0x1000': ['AE', '1', 'NetworkID'],
5639            '0x1010': ['SH', '1', 'StationName'],
5640            '0x1030': ['LO', '1', 'StudyDescription'],
5641            '0x1032': ['SQ', '1', 'ProcedureCodeSequence'],
5642            '0x103E': ['LO', '1', 'SeriesDescription'],
5643            '0x103F': ['SQ', '1', 'SeriesDescriptionCodeSequence'],
5644            '0x1040': ['LO', '1', 'InstitutionalDepartmentName'],
5645            '0x1048': ['PN', '1-n', 'PhysiciansOfRecord'],
5646            '0x1049': ['SQ', '1', 'PhysiciansOfRecordIdentificationSequence'],
5647            '0x1050': ['PN', '1-n', 'PerformingPhysicianName'],
5648            '0x1052': ['SQ', '1', 'PerformingPhysicianIdentificationSequence'],
5649            '0x1060': ['PN', '1-n', 'NameOfPhysiciansReadingStudy'],
5650            '0x1062': ['SQ', '1', 'PhysiciansReadingStudyIdentificationSequence'],
5651            '0x1070': ['PN', '1-n', 'OperatorsName'],
5652            '0x1072': ['SQ', '1', 'OperatorIdentificationSequence'],
5653            '0x1080': ['LO', '1-n', 'AdmittingDiagnosesDescription'],
5654            '0x1084': ['SQ', '1', 'AdmittingDiagnosesCodeSequence'],
5655            '0x1090': ['LO', '1', 'ManufacturerModelName'],
5656            '0x1100': ['SQ', '1', 'ReferencedResultsSequence'],
5657            '0x1110': ['SQ', '1', 'ReferencedStudySequence'],
5658            '0x1111': ['SQ', '1', 'ReferencedPerformedProcedureStepSequence'],
5659            '0x1115': ['SQ', '1', 'ReferencedSeriesSequence'],
5660            '0x1120': ['SQ', '1', 'ReferencedPatientSequence'],
5661            '0x1125': ['SQ', '1', 'ReferencedVisitSequence'],
5662            '0x1130': ['SQ', '1', 'ReferencedOverlaySequence'],
5663            '0x1134': ['SQ', '1', 'ReferencedStereometricInstanceSequence'],
5664            '0x113A': ['SQ', '1', 'ReferencedWaveformSequence'],
5665            '0x1140': ['SQ', '1', 'ReferencedImageSequence'],
5666            '0x1145': ['SQ', '1', 'ReferencedCurveSequence'],
5667            '0x114A': ['SQ', '1', 'ReferencedInstanceSequence'],
5668            '0x114B': ['SQ', '1', 'ReferencedRealWorldValueMappingInstanceSequence'],
5669            '0x1150': ['UI', '1', 'ReferencedSOPClassUID'],
5670            '0x1155': ['UI', '1', 'ReferencedSOPInstanceUID'],
5671            '0x115A': ['UI', '1-n', 'SOPClassesSupported'],
5672            '0x1160': ['IS', '1-n', 'ReferencedFrameNumber'],
5673            '0x1161': ['UL', '1-n', 'SimpleFrameList'],
5674            '0x1162': ['UL', '3-3n', 'CalculatedFrameList'],
5675            '0x1163': ['FD', '2', 'TimeRange'],
5676            '0x1164': ['SQ', '1', 'FrameExtractionSequence'],
5677            '0x1167': ['UI', '1', 'MultiFrameSourceSOPInstanceUID'],
5678            '0x1190': ['UR', '1', 'RetrieveURL'],
5679            '0x1195': ['UI', '1', 'TransactionUID'],
5680            '0x1196': ['US', '1', 'WarningReason'],
5681            '0x1197': ['US', '1', 'FailureReason'],
5682            '0x1198': ['SQ', '1', 'FailedSOPSequence'],
5683            '0x1199': ['SQ', '1', 'ReferencedSOPSequence'],
5684            '0x1200': ['SQ', '1', 'StudiesContainingOtherReferencedInstancesSequence'],
5685            '0x1250': ['SQ', '1', 'RelatedSeriesSequence'],
5686            '0x2110': ['CS', '1', 'LossyImageCompressionRetired'],
5687            '0x2111': ['ST', '1', 'DerivationDescription'],
5688            '0x2112': ['SQ', '1', 'SourceImageSequence'],
5689            '0x2120': ['SH', '1', 'StageName'],
5690            '0x2122': ['IS', '1', 'StageNumber'],
5691            '0x2124': ['IS', '1', 'NumberOfStages'],
5692            '0x2127': ['SH', '1', 'ViewName'],
5693            '0x2128': ['IS', '1', 'ViewNumber'],
5694            '0x2129': ['IS', '1', 'NumberOfEventTimers'],
5695            '0x212A': ['IS', '1', 'NumberOfViewsInStage'],
5696            '0x2130': ['DS', '1-n', 'EventElapsedTimes'],
5697            '0x2132': ['LO', '1-n', 'EventTimerNames'],
5698            '0x2133': ['SQ', '1', 'EventTimerSequence'],
5699            '0x2134': ['FD', '1', 'EventTimeOffset'],
5700            '0x2135': ['SQ', '1', 'EventCodeSequence'],
5701            '0x2142': ['IS', '1', 'StartTrim'],
5702            '0x2143': ['IS', '1', 'StopTrim'],
5703            '0x2144': ['IS', '1', 'RecommendedDisplayFrameRate'],
5704            '0x2200': ['CS', '1', 'TransducerPosition'],
5705            '0x2204': ['CS', '1', 'TransducerOrientation'],
5706            '0x2208': ['CS', '1', 'AnatomicStructure'],
5707            '0x2218': ['SQ', '1', 'AnatomicRegionSequence'],
5708            '0x2220': ['SQ', '1', 'AnatomicRegionModifierSequence'],
5709            '0x2228': ['SQ', '1', 'PrimaryAnatomicStructureSequence'],
5710            '0x2229': ['SQ', '1', 'AnatomicStructureSpaceOrRegionSequence'],
5711            '0x2230': ['SQ', '1', 'PrimaryAnatomicStructureModifierSequence'],
5712            '0x2240': ['SQ', '1', 'TransducerPositionSequence'],
5713            '0x2242': ['SQ', '1', 'TransducerPositionModifierSequence'],
5714            '0x2244': ['SQ', '1', 'TransducerOrientationSequence'],
5715            '0x2246': ['SQ', '1', 'TransducerOrientationModifierSequence'],
5716            '0x2251': ['SQ', '1', 'AnatomicStructureSpaceOrRegionCodeSequenceTrial'],
5717            '0x2253': ['SQ', '1', 'AnatomicPortalOfEntranceCodeSequenceTrial'],
5718            '0x2255': ['SQ', '1', 'AnatomicApproachDirectionCodeSequenceTrial'],
5719            '0x2256': ['ST', '1', 'AnatomicPerspectiveDescriptionTrial'],
5720            '0x2257': ['SQ', '1', 'AnatomicPerspectiveCodeSequenceTrial'],
5721            '0x2258': ['ST', '1', 'AnatomicLocationOfExaminingInstrumentDescriptionTrial'],
5722            '0x2259': ['SQ', '1', 'AnatomicLocationOfExaminingInstrumentCodeSequenceTrial'],
5723            '0x225A': ['SQ', '1', 'AnatomicStructureSpaceOrRegionModifierCodeSequenceTrial'],
5724            '0x225C': ['SQ', '1', 'OnAxisBackgroundAnatomicStructureCodeSequenceTrial'],
5725            '0x3001': ['SQ', '1', 'AlternateRepresentationSequence'],
5726            '0x3010': ['UI', '1-n', 'IrradiationEventUID'],
5727            '0x3011': ['SQ', '1', 'SourceIrradiationEventSequence'],
5728            '0x3012': ['UI', '1', 'RadiopharmaceuticalAdministrationEventUID'],
5729            '0x4000': ['LT', '1', 'IdentifyingComments'],
5730            '0x9007': ['CS', '4', 'FrameType'],
5731            '0x9092': ['SQ', '1', 'ReferencedImageEvidenceSequence'],
5732            '0x9121': ['SQ', '1', 'ReferencedRawDataSequence'],
5733            '0x9123': ['UI', '1', 'CreatorVersionUID'],
5734            '0x9124': ['SQ', '1', 'DerivationImageSequence'],
5735            '0x9154': ['SQ', '1', 'SourceImageEvidenceSequence'],
5736            '0x9205': ['CS', '1', 'PixelPresentation'],
5737            '0x9206': ['CS', '1', 'VolumetricProperties'],
5738            '0x9207': ['CS', '1', 'VolumeBasedCalculationTechnique'],
5739            '0x9208': ['CS', '1', 'ComplexImageComponent'],
5740            '0x9209': ['CS', '1', 'AcquisitionContrast'],
5741            '0x9215': ['SQ', '1', 'DerivationCodeSequence'],
5742            '0x9237': ['SQ', '1', 'ReferencedPresentationStateSequence'],
5743            '0x9410': ['SQ', '1', 'ReferencedOtherPlaneSequence'],
5744            '0x9458': ['SQ', '1', 'FrameDisplaySequence'],
5745            '0x9459': ['FL', '1', 'RecommendedDisplayFrameRateInFloat'],
5746            '0x9460': ['CS', '1', 'SkipFrameRangeFlag']
5747        },
5748        '0x0010': {
5749            '0x0000': ['UL', '1', 'GenericGroupLength'],
5750            '0x0010': ['PN', '1', 'PatientName'],
5751            '0x0020': ['LO', '1', 'PatientID'],
5752            '0x0021': ['LO', '1', 'IssuerOfPatientID'],
5753            '0x0022': ['CS', '1', 'TypeOfPatientID'],
5754            '0x0024': ['SQ', '1', 'IssuerOfPatientIDQualifiersSequence'],
5755            '0x0030': ['DA', '1', 'PatientBirthDate'],
5756            '0x0032': ['TM', '1', 'PatientBirthTime'],
5757            '0x0040': ['CS', '1', 'PatientSex'],
5758            '0x0050': ['SQ', '1', 'PatientInsurancePlanCodeSequence'],
5759            '0x0101': ['SQ', '1', 'PatientPrimaryLanguageCodeSequence'],
5760            '0x0102': ['SQ', '1', 'PatientPrimaryLanguageModifierCodeSequence'],
5761            '0x0200': ['CS', '1', 'QualityControlSubject'],
5762            '0x0201': ['SQ', '1', 'QualityControlSubjectTypeCodeSequence'],
5763            '0x1000': ['LO', '1-n', 'OtherPatientIDs'],
5764            '0x1001': ['PN', '1-n', 'OtherPatientNames'],
5765            '0x1002': ['SQ', '1', 'OtherPatientIDsSequence'],
5766            '0x1005': ['PN', '1', 'PatientBirthName'],
5767            '0x1010': ['AS', '1', 'PatientAge'],
5768            '0x1020': ['DS', '1', 'PatientSize'],
5769            '0x1021': ['SQ', '1', 'PatientSizeCodeSequence'],
5770            '0x1030': ['DS', '1', 'PatientWeight'],
5771            '0x1040': ['LO', '1', 'PatientAddress'],
5772            '0x1050': ['LO', '1-n', 'InsurancePlanIdentification'],
5773            '0x1060': ['PN', '1', 'PatientMotherBirthName'],
5774            '0x1080': ['LO', '1', 'MilitaryRank'],
5775            '0x1081': ['LO', '1', 'BranchOfService'],
5776            '0x1090': ['LO', '1', 'MedicalRecordLocator'],
5777            '0x1100': ['SQ', '1', 'ReferencedPatientPhotoSequence'],
5778            '0x2000': ['LO', '1-n', 'MedicalAlerts'],
5779            '0x2110': ['LO', '1-n', 'Allergies'],
5780            '0x2150': ['LO', '1', 'CountryOfResidence'],
5781            '0x2152': ['LO', '1', 'RegionOfResidence'],
5782            '0x2154': ['SH', '1-n', 'PatientTelephoneNumbers'],
5783            '0x2155': ['LT', '1', 'PatientTelecomInformation'],
5784            '0x2160': ['SH', '1', 'EthnicGroup'],
5785            '0x2180': ['SH', '1', 'Occupation'],
5786            '0x21A0': ['CS', '1', 'SmokingStatus'],
5787            '0x21B0': ['LT', '1', 'AdditionalPatientHistory'],
5788            '0x21C0': ['US', '1', 'PregnancyStatus'],
5789            '0x21D0': ['DA', '1', 'LastMenstrualDate'],
5790            '0x21F0': ['LO', '1', 'PatientReligiousPreference'],
5791            '0x2201': ['LO', '1', 'PatientSpeciesDescription'],
5792            '0x2202': ['SQ', '1', 'PatientSpeciesCodeSequence'],
5793            '0x2203': ['CS', '1', 'PatientSexNeutered'],
5794            '0x2210': ['CS', '1', 'AnatomicalOrientationType'],
5795            '0x2292': ['LO', '1', 'PatientBreedDescription'],
5796            '0x2293': ['SQ', '1', 'PatientBreedCodeSequence'],
5797            '0x2294': ['SQ', '1', 'BreedRegistrationSequence'],
5798            '0x2295': ['LO', '1', 'BreedRegistrationNumber'],
5799            '0x2296': ['SQ', '1', 'BreedRegistryCodeSequence'],
5800            '0x2297': ['PN', '1', 'ResponsiblePerson'],
5801            '0x2298': ['CS', '1', 'ResponsiblePersonRole'],
5802            '0x2299': ['LO', '1', 'ResponsibleOrganization'],
5803            '0x4000': ['LT', '1', 'PatientComments'],
5804            '0x9431': ['FL', '1', 'ExaminedBodyThickness']
5805        },
5806        '0x0012': {
5807            '0x0000': ['UL', '1', 'GenericGroupLength'],
5808            '0x0010': ['LO', '1', 'ClinicalTrialSponsorName'],
5809            '0x0020': ['LO', '1', 'ClinicalTrialProtocolID'],
5810            '0x0021': ['LO', '1', 'ClinicalTrialProtocolName'],
5811            '0x0030': ['LO', '1', 'ClinicalTrialSiteID'],
5812            '0x0031': ['LO', '1', 'ClinicalTrialSiteName'],
5813            '0x0040': ['LO', '1', 'ClinicalTrialSubjectID'],
5814            '0x0042': ['LO', '1', 'ClinicalTrialSubjectReadingID'],
5815            '0x0050': ['LO', '1', 'ClinicalTrialTimePointID'],
5816            '0x0051': ['ST', '1', 'ClinicalTrialTimePointDescription'],
5817            '0x0060': ['LO', '1', 'ClinicalTrialCoordinatingCenterName'],
5818            '0x0062': ['CS', '1', 'PatientIdentityRemoved'],
5819            '0x0063': ['LO', '1-n', 'DeidentificationMethod'],
5820            '0x0064': ['SQ', '1', 'DeidentificationMethodCodeSequence'],
5821            '0x0071': ['LO', '1', 'ClinicalTrialSeriesID'],
5822            '0x0072': ['LO', '1', 'ClinicalTrialSeriesDescription'],
5823            '0x0081': ['LO', '1', 'ClinicalTrialProtocolEthicsCommitteeName'],
5824            '0x0082': ['LO', '1', 'ClinicalTrialProtocolEthicsCommitteeApprovalNumber'],
5825            '0x0083': ['SQ', '1', 'ConsentForClinicalTrialUseSequence'],
5826            '0x0084': ['CS', '1', 'DistributionType'],
5827            '0x0085': ['CS', '1', 'ConsentForDistributionFlag']
5828        },
5829        '0x0014': {
5830            '0x0000': ['UL', '1', 'GenericGroupLength'],
5831            '0x0023': ['ST', '1-n', 'CADFileFormat'],
5832            '0x0024': ['ST', '1-n', 'ComponentReferenceSystem'],
5833            '0x0025': ['ST', '1-n', 'ComponentManufacturingProcedure'],
5834            '0x0028': ['ST', '1-n', 'ComponentManufacturer'],
5835            '0x0030': ['DS', '1-n', 'MaterialThickness'],
5836            '0x0032': ['DS', '1-n', 'MaterialPipeDiameter'],
5837            '0x0034': ['DS', '1-n', 'MaterialIsolationDiameter'],
5838            '0x0042': ['ST', '1-n', 'MaterialGrade'],
5839            '0x0044': ['ST', '1-n', 'MaterialPropertiesDescription'],
5840            '0x0045': ['ST', '1-n', 'MaterialPropertiesFileFormatRetired'],
5841            '0x0046': ['LT', '1', 'MaterialNotes'],
5842            '0x0050': ['CS', '1', 'ComponentShape'],
5843            '0x0052': ['CS', '1', 'CurvatureType'],
5844            '0x0054': ['DS', '1', 'OuterDiameter'],
5845            '0x0056': ['DS', '1', 'InnerDiameter'],
5846            '0x1010': ['ST', '1', 'ActualEnvironmentalConditions'],
5847            '0x1020': ['DA', '1', 'ExpiryDate'],
5848            '0x1040': ['ST', '1', 'EnvironmentalConditions'],
5849            '0x2002': ['SQ', '1', 'EvaluatorSequence'],
5850            '0x2004': ['IS', '1', 'EvaluatorNumber'],
5851            '0x2006': ['PN', '1', 'EvaluatorName'],
5852            '0x2008': ['IS', '1', 'EvaluationAttempt'],
5853            '0x2012': ['SQ', '1', 'IndicationSequence'],
5854            '0x2014': ['IS', '1', 'IndicationNumber'],
5855            '0x2016': ['SH', '1', 'IndicationLabel'],
5856            '0x2018': ['ST', '1', 'IndicationDescription'],
5857            '0x201A': ['CS', '1-n', 'IndicationType'],
5858            '0x201C': ['CS', '1', 'IndicationDisposition'],
5859            '0x201E': ['SQ', '1', 'IndicationROISequence'],
5860            '0x2030': ['SQ', '1', 'IndicationPhysicalPropertySequence'],
5861            '0x2032': ['SH', '1', 'PropertyLabel'],
5862            '0x2202': ['IS', '1', 'CoordinateSystemNumberOfAxes'],
5863            '0x2204': ['SQ', '1', 'CoordinateSystemAxesSequence'],
5864            '0x2206': ['ST', '1', 'CoordinateSystemAxisDescription'],
5865            '0x2208': ['CS', '1', 'CoordinateSystemDataSetMapping'],
5866            '0x220A': ['IS', '1', 'CoordinateSystemAxisNumber'],
5867            '0x220C': ['CS', '1', 'CoordinateSystemAxisType'],
5868            '0x220E': ['CS', '1', 'CoordinateSystemAxisUnits'],
5869            '0x2210': ['OB', '1', 'CoordinateSystemAxisValues'],
5870            '0x2220': ['SQ', '1', 'CoordinateSystemTransformSequence'],
5871            '0x2222': ['ST', '1', 'TransformDescription'],
5872            '0x2224': ['IS', '1', 'TransformNumberOfAxes'],
5873            '0x2226': ['IS', '1-n', 'TransformOrderOfAxes'],
5874            '0x2228': ['CS', '1', 'TransformedAxisUnits'],
5875            '0x222A': ['DS', '1-n', 'CoordinateSystemTransformRotationAndScaleMatrix'],
5876            '0x222C': ['DS', '1-n', 'CoordinateSystemTransformTranslationMatrix'],
5877            '0x3011': ['DS', '1', 'InternalDetectorFrameTime'],
5878            '0x3012': ['DS', '1', 'NumberOfFramesIntegrated'],
5879            '0x3020': ['SQ', '1', 'DetectorTemperatureSequence'],
5880            '0x3022': ['ST', '1', 'SensorName'],
5881            '0x3024': ['DS', '1', 'HorizontalOffsetOfSensor'],
5882            '0x3026': ['DS', '1', 'VerticalOffsetOfSensor'],
5883            '0x3028': ['DS', '1', 'SensorTemperature'],
5884            '0x3040': ['SQ', '1', 'DarkCurrentSequence'],
5885            '0x3050': ['ox', '1', 'DarkCurrentCounts'],
5886            '0x3060': ['SQ', '1', 'GainCorrectionReferenceSequence'],
5887            '0x3070': ['ox', '1', 'AirCounts'],
5888            '0x3071': ['DS', '1', 'KVUsedInGainCalibration'],
5889            '0x3072': ['DS', '1', 'MAUsedInGainCalibration'],
5890            '0x3073': ['DS', '1', 'NumberOfFramesUsedForIntegration'],
5891            '0x3074': ['LO', '1', 'FilterMaterialUsedInGainCalibration'],
5892            '0x3075': ['DS', '1', 'FilterThicknessUsedInGainCalibration'],
5893            '0x3076': ['DA', '1', 'DateOfGainCalibration'],
5894            '0x3077': ['TM', '1', 'TimeOfGainCalibration'],
5895            '0x3080': ['OB', '1', 'BadPixelImage'],
5896            '0x3099': ['LT', '1', 'CalibrationNotes'],
5897            '0x4002': ['SQ', '1', 'PulserEquipmentSequence'],
5898            '0x4004': ['CS', '1', 'PulserType'],
5899            '0x4006': ['LT', '1', 'PulserNotes'],
5900            '0x4008': ['SQ', '1', 'ReceiverEquipmentSequence'],
5901            '0x400A': ['CS', '1', 'AmplifierType'],
5902            '0x400C': ['LT', '1', 'ReceiverNotes'],
5903            '0x400E': ['SQ', '1', 'PreAmplifierEquipmentSequence'],
5904            '0x400F': ['LT', '1', 'PreAmplifierNotes'],
5905            '0x4010': ['SQ', '1', 'TransmitTransducerSequence'],
5906            '0x4011': ['SQ', '1', 'ReceiveTransducerSequence'],
5907            '0x4012': ['US', '1', 'NumberOfElements'],
5908            '0x4013': ['CS', '1', 'ElementShape'],
5909            '0x4014': ['DS', '1', 'ElementDimensionA'],
5910            '0x4015': ['DS', '1', 'ElementDimensionB'],
5911            '0x4016': ['DS', '1', 'ElementPitchA'],
5912            '0x4017': ['DS', '1', 'MeasuredBeamDimensionA'],
5913            '0x4018': ['DS', '1', 'MeasuredBeamDimensionB'],
5914            '0x4019': ['DS', '1', 'LocationOfMeasuredBeamDiameter'],
5915            '0x401A': ['DS', '1', 'NominalFrequency'],
5916            '0x401B': ['DS', '1', 'MeasuredCenterFrequency'],
5917            '0x401C': ['DS', '1', 'MeasuredBandwidth'],
5918            '0x401D': ['DS', '1', 'ElementPitchB'],
5919            '0x4020': ['SQ', '1', 'PulserSettingsSequence'],
5920            '0x4022': ['DS', '1', 'PulseWidth'],
5921            '0x4024': ['DS', '1', 'ExcitationFrequency'],
5922            '0x4026': ['CS', '1', 'ModulationType'],
5923            '0x4028': ['DS', '1', 'Damping'],
5924            '0x4030': ['SQ', '1', 'ReceiverSettingsSequence'],
5925            '0x4031': ['DS', '1', 'AcquiredSoundpathLength'],
5926            '0x4032': ['CS', '1', 'AcquisitionCompressionType'],
5927            '0x4033': ['IS', '1', 'AcquisitionSampleSize'],
5928            '0x4034': ['DS', '1', 'RectifierSmoothing'],
5929            '0x4035': ['SQ', '1', 'DACSequence'],
5930            '0x4036': ['CS', '1', 'DACType'],
5931            '0x4038': ['DS', '1-n', 'DACGainPoints'],
5932            '0x403A': ['DS', '1-n', 'DACTimePoints'],
5933            '0x403C': ['DS', '1-n', 'DACAmplitude'],
5934            '0x4040': ['SQ', '1', 'PreAmplifierSettingsSequence'],
5935            '0x4050': ['SQ', '1', 'TransmitTransducerSettingsSequence'],
5936            '0x4051': ['SQ', '1', 'ReceiveTransducerSettingsSequence'],
5937            '0x4052': ['DS', '1', 'IncidentAngle'],
5938            '0x4054': ['ST', '1', 'CouplingTechnique'],
5939            '0x4056': ['ST', '1', 'CouplingMedium'],
5940            '0x4057': ['DS', '1', 'CouplingVelocity'],
5941            '0x4058': ['DS', '1', 'ProbeCenterLocationX'],
5942            '0x4059': ['DS', '1', 'ProbeCenterLocationZ'],
5943            '0x405A': ['DS', '1', 'SoundPathLength'],
5944            '0x405C': ['ST', '1', 'DelayLawIdentifier'],
5945            '0x4060': ['SQ', '1', 'GateSettingsSequence'],
5946            '0x4062': ['DS', '1', 'GateThreshold'],
5947            '0x4064': ['DS', '1', 'VelocityOfSound'],
5948            '0x4070': ['SQ', '1', 'CalibrationSettingsSequence'],
5949            '0x4072': ['ST', '1', 'CalibrationProcedure'],
5950            '0x4074': ['SH', '1', 'ProcedureVersion'],
5951            '0x4076': ['DA', '1', 'ProcedureCreationDate'],
5952            '0x4078': ['DA', '1', 'ProcedureExpirationDate'],
5953            '0x407A': ['DA', '1', 'ProcedureLastModifiedDate'],
5954            '0x407C': ['TM', '1-n', 'CalibrationTime'],
5955            '0x407E': ['DA', '1-n', 'CalibrationDate'],
5956            '0x4080': ['SQ', '1', 'ProbeDriveEquipmentSequence'],
5957            '0x4081': ['CS', '1', 'DriveType'],
5958            '0x4082': ['LT', '1', 'ProbeDriveNotes'],
5959            '0x4083': ['SQ', '1', 'DriveProbeSequence'],
5960            '0x4084': ['DS', '1', 'ProbeInductance'],
5961            '0x4085': ['DS', '1', 'ProbeResistance'],
5962            '0x4086': ['SQ', '1', 'ReceiveProbeSequence'],
5963            '0x4087': ['SQ', '1', 'ProbeDriveSettingsSequence'],
5964            '0x4088': ['DS', '1', 'BridgeResistors'],
5965            '0x4089': ['DS', '1', 'ProbeOrientationAngle'],
5966            '0x408B': ['DS', '1', 'UserSelectedGainY'],
5967            '0x408C': ['DS', '1', 'UserSelectedPhase'],
5968            '0x408D': ['DS', '1', 'UserSelectedOffsetX'],
5969            '0x408E': ['DS', '1', 'UserSelectedOffsetY'],
5970            '0x4091': ['SQ', '1', 'ChannelSettingsSequence'],
5971            '0x4092': ['DS', '1', 'ChannelThreshold'],
5972            '0x409A': ['SQ', '1', 'ScannerSettingsSequence'],
5973            '0x409B': ['ST', '1', 'ScanProcedure'],
5974            '0x409C': ['DS', '1', 'TranslationRateX'],
5975            '0x409D': ['DS', '1', 'TranslationRateY'],
5976            '0x409F': ['DS', '1', 'ChannelOverlap'],
5977            '0x40A0': ['LO', '1', 'ImageQualityIndicatorType'],
5978            '0x40A1': ['LO', '1', 'ImageQualityIndicatorMaterial'],
5979            '0x40A2': ['LO', '1', 'ImageQualityIndicatorSize'],
5980            '0x5002': ['IS', '1', 'LINACEnergy'],
5981            '0x5004': ['IS', '1', 'LINACOutput'],
5982            '0x5100': ['US', '1', 'ActiveAperture'],
5983            '0x5101': ['DS', '1', 'TotalAperture'],
5984            '0x5102': ['DS', '1', 'ApertureElevation'],
5985            '0x5103': ['DS', '1', 'MainLobeAngle'],
5986            '0x5104': ['DS', '1', 'MainRoofAngle'],
5987            '0x5105': ['CS', '1', 'ConnectorType'],
5988            '0x5106': ['SH', '1', 'WedgeModelNumber'],
5989            '0x5107': ['DS', '1', 'WedgeAngleFloat'],
5990            '0x5108': ['DS', '1', 'WedgeRoofAngle'],
5991            '0x5109': ['CS', '1', 'WedgeElement1Position'],
5992            '0x510A': ['DS', '1', 'WedgeMaterialVelocity'],
5993            '0x510B': ['SH', '1', 'WedgeMaterial'],
5994            '0x510C': ['DS', '1', 'WedgeOffsetZ'],
5995            '0x510D': ['DS', '1', 'WedgeOriginOffsetX'],
5996            '0x510E': ['DS', '1', 'WedgeTimeDelay'],
5997            '0x510F': ['SH', '1', 'WedgeName'],
5998            '0x5110': ['SH', '1', 'WedgeManufacturerName'],
5999            '0x5111': ['LO', '1', 'WedgeDescription'],
6000            '0x5112': ['DS', '1', 'NominalBeamAngle'],
6001            '0x5113': ['DS', '1', 'WedgeOffsetX'],
6002            '0x5114': ['DS', '1', 'WedgeOffsetY'],
6003            '0x5115': ['DS', '1', 'WedgeTotalLength'],
6004            '0x5116': ['DS', '1', 'WedgeInContactLength'],
6005            '0x5117': ['DS', '1', 'WedgeFrontGap'],
6006            '0x5118': ['DS', '1', 'WedgeTotalHeight'],
6007            '0x5119': ['DS', '1', 'WedgeFrontHeight'],
6008            '0x511A': ['DS', '1', 'WedgeRearHeight'],
6009            '0x511B': ['DS', '1', 'WedgeTotalWidth'],
6010            '0x511C': ['DS', '1', 'WedgeInContactWidth'],
6011            '0x511D': ['DS', '1', 'WedgeChamferHeight'],
6012            '0x511E': ['CS', '1', 'WedgeCurve'],
6013            '0x511F': ['DS', '1', 'RadiusAlongWedge']
6014        },
6015        '0x0018': {
6016            '0x0000': ['UL', '1', 'GenericGroupLength'],
6017            '0x0010': ['LO', '1', 'ContrastBolusAgent'],
6018            '0x0012': ['SQ', '1', 'ContrastBolusAgentSequence'],
6019            '0x0013': ['FL', '1', 'ContrastBolusT1Relaxivity'],
6020            '0x0014': ['SQ', '1', 'ContrastBolusAdministrationRouteSequence'],
6021            '0x0015': ['CS', '1', 'BodyPartExamined'],
6022            '0x0020': ['CS', '1-n', 'ScanningSequence'],
6023            '0x0021': ['CS', '1-n', 'SequenceVariant'],
6024            '0x0022': ['CS', '1-n', 'ScanOptions'],
6025            '0x0023': ['CS', '1', 'MRAcquisitionType'],
6026            '0x0024': ['SH', '1', 'SequenceName'],
6027            '0x0025': ['CS', '1', 'AngioFlag'],
6028            '0x0026': ['SQ', '1', 'InterventionDrugInformationSequence'],
6029            '0x0027': ['TM', '1', 'InterventionDrugStopTime'],
6030            '0x0028': ['DS', '1', 'InterventionDrugDose'],
6031            '0x0029': ['SQ', '1', 'InterventionDrugCodeSequence'],
6032            '0x002A': ['SQ', '1', 'AdditionalDrugSequence'],
6033            '0x0030': ['LO', '1-n', 'Radionuclide'],
6034            '0x0031': ['LO', '1', 'Radiopharmaceutical'],
6035            '0x0032': ['DS', '1', 'EnergyWindowCenterline'],
6036            '0x0033': ['DS', '1-n', 'EnergyWindowTotalWidth'],
6037            '0x0034': ['LO', '1', 'InterventionDrugName'],
6038            '0x0035': ['TM', '1', 'InterventionDrugStartTime'],
6039            '0x0036': ['SQ', '1', 'InterventionSequence'],
6040            '0x0037': ['CS', '1', 'TherapyType'],
6041            '0x0038': ['CS', '1', 'InterventionStatus'],
6042            '0x0039': ['CS', '1', 'TherapyDescription'],
6043            '0x003A': ['ST', '1', 'InterventionDescription'],
6044            '0x0040': ['IS', '1', 'CineRate'],
6045            '0x0042': ['CS', '1', 'InitialCineRunState'],
6046            '0x0050': ['DS', '1', 'SliceThickness'],
6047            '0x0060': ['DS', '1', 'KVP'],
6048            '0x0070': ['IS', '1', 'CountsAccumulated'],
6049            '0x0071': ['CS', '1', 'AcquisitionTerminationCondition'],
6050            '0x0072': ['DS', '1', 'EffectiveDuration'],
6051            '0x0073': ['CS', '1', 'AcquisitionStartCondition'],
6052            '0x0074': ['IS', '1', 'AcquisitionStartConditionData'],
6053            '0x0075': ['IS', '1', 'AcquisitionTerminationConditionData'],
6054            '0x0080': ['DS', '1', 'RepetitionTime'],
6055            '0x0081': ['DS', '1', 'EchoTime'],
6056            '0x0082': ['DS', '1', 'InversionTime'],
6057            '0x0083': ['DS', '1', 'NumberOfAverages'],
6058            '0x0084': ['DS', '1', 'ImagingFrequency'],
6059            '0x0085': ['SH', '1', 'ImagedNucleus'],
6060            '0x0086': ['IS', '1-n', 'EchoNumbers'],
6061            '0x0087': ['DS', '1', 'MagneticFieldStrength'],
6062            '0x0088': ['DS', '1', 'SpacingBetweenSlices'],
6063            '0x0089': ['IS', '1', 'NumberOfPhaseEncodingSteps'],
6064            '0x0090': ['DS', '1', 'DataCollectionDiameter'],
6065            '0x0091': ['IS', '1', 'EchoTrainLength'],
6066            '0x0093': ['DS', '1', 'PercentSampling'],
6067            '0x0094': ['DS', '1', 'PercentPhaseFieldOfView'],
6068            '0x0095': ['DS', '1', 'PixelBandwidth'],
6069            '0x1000': ['LO', '1', 'DeviceSerialNumber'],
6070            '0x1002': ['UI', '1', 'DeviceUID'],
6071            '0x1003': ['LO', '1', 'DeviceID'],
6072            '0x1004': ['LO', '1', 'PlateID'],
6073            '0x1005': ['LO', '1', 'GeneratorID'],
6074            '0x1006': ['LO', '1', 'GridID'],
6075            '0x1007': ['LO', '1', 'CassetteID'],
6076            '0x1008': ['LO', '1', 'GantryID'],
6077            '0x1010': ['LO', '1', 'SecondaryCaptureDeviceID'],
6078            '0x1011': ['LO', '1', 'HardcopyCreationDeviceID'],
6079            '0x1012': ['DA', '1', 'DateOfSecondaryCapture'],
6080            '0x1014': ['TM', '1', 'TimeOfSecondaryCapture'],
6081            '0x1016': ['LO', '1', 'SecondaryCaptureDeviceManufacturer'],
6082            '0x1017': ['LO', '1', 'HardcopyDeviceManufacturer'],
6083            '0x1018': ['LO', '1', 'SecondaryCaptureDeviceManufacturerModelName'],
6084            '0x1019': ['LO', '1-n', 'SecondaryCaptureDeviceSoftwareVersions'],
6085            '0x101A': ['LO', '1-n', 'HardcopyDeviceSoftwareVersion'],
6086            '0x101B': ['LO', '1', 'HardcopyDeviceManufacturerModelName'],
6087            '0x1020': ['LO', '1-n', 'SoftwareVersions'],
6088            '0x1022': ['SH', '1', 'VideoImageFormatAcquired'],
6089            '0x1023': ['LO', '1', 'DigitalImageFormatAcquired'],
6090            '0x1030': ['LO', '1', 'ProtocolName'],
6091            '0x1040': ['LO', '1', 'ContrastBolusRoute'],
6092            '0x1041': ['DS', '1', 'ContrastBolusVolume'],
6093            '0x1042': ['TM', '1', 'ContrastBolusStartTime'],
6094            '0x1043': ['TM', '1', 'ContrastBolusStopTime'],
6095            '0x1044': ['DS', '1', 'ContrastBolusTotalDose'],
6096            '0x1045': ['IS', '1', 'SyringeCounts'],
6097            '0x1046': ['DS', '1-n', 'ContrastFlowRate'],
6098            '0x1047': ['DS', '1-n', 'ContrastFlowDuration'],
6099            '0x1048': ['CS', '1', 'ContrastBolusIngredient'],
6100            '0x1049': ['DS', '1', 'ContrastBolusIngredientConcentration'],
6101            '0x1050': ['DS', '1', 'SpatialResolution'],
6102            '0x1060': ['DS', '1', 'TriggerTime'],
6103            '0x1061': ['LO', '1', 'TriggerSourceOrType'],
6104            '0x1062': ['IS', '1', 'NominalInterval'],
6105            '0x1063': ['DS', '1', 'FrameTime'],
6106            '0x1064': ['LO', '1', 'CardiacFramingType'],
6107            '0x1065': ['DS', '1-n', 'FrameTimeVector'],
6108            '0x1066': ['DS', '1', 'FrameDelay'],
6109            '0x1067': ['DS', '1', 'ImageTriggerDelay'],
6110            '0x1068': ['DS', '1', 'MultiplexGroupTimeOffset'],
6111            '0x1069': ['DS', '1', 'TriggerTimeOffset'],
6112            '0x106A': ['CS', '1', 'SynchronizationTrigger'],
6113            '0x106C': ['US', '2', 'SynchronizationChannel'],
6114            '0x106E': ['UL', '1', 'TriggerSamplePosition'],
6115            '0x1070': ['LO', '1', 'RadiopharmaceuticalRoute'],
6116            '0x1071': ['DS', '1', 'RadiopharmaceuticalVolume'],
6117            '0x1072': ['TM', '1', 'RadiopharmaceuticalStartTime'],
6118            '0x1073': ['TM', '1', 'RadiopharmaceuticalStopTime'],
6119            '0x1074': ['DS', '1', 'RadionuclideTotalDose'],
6120            '0x1075': ['DS', '1', 'RadionuclideHalfLife'],
6121            '0x1076': ['DS', '1', 'RadionuclidePositronFraction'],
6122            '0x1077': ['DS', '1', 'RadiopharmaceuticalSpecificActivity'],
6123            '0x1078': ['DT', '1', 'RadiopharmaceuticalStartDateTime'],
6124            '0x1079': ['DT', '1', 'RadiopharmaceuticalStopDateTime'],
6125            '0x1080': ['CS', '1', 'BeatRejectionFlag'],
6126            '0x1081': ['IS', '1', 'LowRRValue'],
6127            '0x1082': ['IS', '1', 'HighRRValue'],
6128            '0x1083': ['IS', '1', 'IntervalsAcquired'],
6129            '0x1084': ['IS', '1', 'IntervalsRejected'],
6130            '0x1085': ['LO', '1', 'PVCRejection'],
6131            '0x1086': ['IS', '1', 'SkipBeats'],
6132            '0x1088': ['IS', '1', 'HeartRate'],
6133            '0x1090': ['IS', '1', 'CardiacNumberOfImages'],
6134            '0x1094': ['IS', '1', 'TriggerWindow'],
6135            '0x1100': ['DS', '1', 'ReconstructionDiameter'],
6136            '0x1110': ['DS', '1', 'DistanceSourceToDetector'],
6137            '0x1111': ['DS', '1', 'DistanceSourceToPatient'],
6138            '0x1114': ['DS', '1', 'EstimatedRadiographicMagnificationFactor'],
6139            '0x1120': ['DS', '1', 'GantryDetectorTilt'],
6140            '0x1121': ['DS', '1', 'GantryDetectorSlew'],
6141            '0x1130': ['DS', '1', 'TableHeight'],
6142            '0x1131': ['DS', '1', 'TableTraverse'],
6143            '0x1134': ['CS', '1', 'TableMotion'],
6144            '0x1135': ['DS', '1-n', 'TableVerticalIncrement'],
6145            '0x1136': ['DS', '1-n', 'TableLateralIncrement'],
6146            '0x1137': ['DS', '1-n', 'TableLongitudinalIncrement'],
6147            '0x1138': ['DS', '1', 'TableAngle'],
6148            '0x113A': ['CS', '1', 'TableType'],
6149            '0x1140': ['CS', '1', 'RotationDirection'],
6150            '0x1141': ['DS', '1', 'AngularPosition'],
6151            '0x1142': ['DS', '1-n', 'RadialPosition'],
6152            '0x1143': ['DS', '1', 'ScanArc'],
6153            '0x1144': ['DS', '1', 'AngularStep'],
6154            '0x1145': ['DS', '1', 'CenterOfRotationOffset'],
6155            '0x1146': ['DS', '1-n', 'RotationOffset'],
6156            '0x1147': ['CS', '1', 'FieldOfViewShape'],
6157            '0x1149': ['IS', '1-2', 'FieldOfViewDimensions'],
6158            '0x1150': ['IS', '1', 'ExposureTime'],
6159            '0x1151': ['IS', '1', 'XRayTubeCurrent'],
6160            '0x1152': ['IS', '1', 'Exposure'],
6161            '0x1153': ['IS', '1', 'ExposureInuAs'],
6162            '0x1154': ['DS', '1', 'AveragePulseWidth'],
6163            '0x1155': ['CS', '1', 'RadiationSetting'],
6164            '0x1156': ['CS', '1', 'RectificationType'],
6165            '0x115A': ['CS', '1', 'RadiationMode'],
6166            '0x115E': ['DS', '1', 'ImageAndFluoroscopyAreaDoseProduct'],
6167            '0x1160': ['SH', '1', 'FilterType'],
6168            '0x1161': ['LO', '1-n', 'TypeOfFilters'],
6169            '0x1162': ['DS', '1', 'IntensifierSize'],
6170            '0x1164': ['DS', '2', 'ImagerPixelSpacing'],
6171            '0x1166': ['CS', '1-n', 'Grid'],
6172            '0x1170': ['IS', '1', 'GeneratorPower'],
6173            '0x1180': ['SH', '1', 'CollimatorGridName'],
6174            '0x1181': ['CS', '1', 'CollimatorType'],
6175            '0x1182': ['IS', '1-2', 'FocalDistance'],
6176            '0x1183': ['DS', '1-2', 'XFocusCenter'],
6177            '0x1184': ['DS', '1-2', 'YFocusCenter'],
6178            '0x1190': ['DS', '1-n', 'FocalSpots'],
6179            '0x1191': ['CS', '1', 'AnodeTargetMaterial'],
6180            '0x11A0': ['DS', '1', 'BodyPartThickness'],
6181            '0x11A2': ['DS', '1', 'CompressionForce'],
6182            '0x11A4': ['LO', '1', 'PaddleDescription'],
6183            '0x1200': ['DA', '1-n', 'DateOfLastCalibration'],
6184            '0x1201': ['TM', '1-n', 'TimeOfLastCalibration'],
6185            '0x1202': ['DT', '1', 'DateTimeOfLastCalibration'],
6186            '0x1210': ['SH', '1-n', 'ConvolutionKernel'],
6187            '0x1240': ['IS', '1-n', 'UpperLowerPixelValues'],
6188            '0x1242': ['IS', '1', 'ActualFrameDuration'],
6189            '0x1243': ['IS', '1', 'CountRate'],
6190            '0x1244': ['US', '1', 'PreferredPlaybackSequencing'],
6191            '0x1250': ['SH', '1', 'ReceiveCoilName'],
6192            '0x1251': ['SH', '1', 'TransmitCoilName'],
6193            '0x1260': ['SH', '1', 'PlateType'],
6194            '0x1261': ['LO', '1', 'PhosphorType'],
6195            '0x1300': ['DS', '1', 'ScanVelocity'],
6196            '0x1301': ['CS', '1-n', 'WholeBodyTechnique'],
6197            '0x1302': ['IS', '1', 'ScanLength'],
6198            '0x1310': ['US', '4', 'AcquisitionMatrix'],
6199            '0x1312': ['CS', '1', 'InPlanePhaseEncodingDirection'],
6200            '0x1314': ['DS', '1', 'FlipAngle'],
6201            '0x1315': ['CS', '1', 'VariableFlipAngleFlag'],
6202            '0x1316': ['DS', '1', 'SAR'],
6203            '0x1318': ['DS', '1', 'dBdt'],
6204            '0x1400': ['LO', '1', 'AcquisitionDeviceProcessingDescription'],
6205            '0x1401': ['LO', '1', 'AcquisitionDeviceProcessingCode'],
6206            '0x1402': ['CS', '1', 'CassetteOrientation'],
6207            '0x1403': ['CS', '1', 'CassetteSize'],
6208            '0x1404': ['US', '1', 'ExposuresOnPlate'],
6209            '0x1405': ['IS', '1', 'RelativeXRayExposure'],
6210            '0x1411': ['DS', '1', 'ExposureIndex'],
6211            '0x1412': ['DS', '1', 'TargetExposureIndex'],
6212            '0x1413': ['DS', '1', 'DeviationIndex'],
6213            '0x1450': ['DS', '1', 'ColumnAngulation'],
6214            '0x1460': ['DS', '1', 'TomoLayerHeight'],
6215            '0x1470': ['DS', '1', 'TomoAngle'],
6216            '0x1480': ['DS', '1', 'TomoTime'],
6217            '0x1490': ['CS', '1', 'TomoType'],
6218            '0x1491': ['CS', '1', 'TomoClass'],
6219            '0x1495': ['IS', '1', 'NumberOfTomosynthesisSourceImages'],
6220            '0x1500': ['CS', '1', 'PositionerMotion'],
6221            '0x1508': ['CS', '1', 'PositionerType'],
6222            '0x1510': ['DS', '1', 'PositionerPrimaryAngle'],
6223            '0x1511': ['DS', '1', 'PositionerSecondaryAngle'],
6224            '0x1520': ['DS', '1-n', 'PositionerPrimaryAngleIncrement'],
6225            '0x1521': ['DS', '1-n', 'PositionerSecondaryAngleIncrement'],
6226            '0x1530': ['DS', '1', 'DetectorPrimaryAngle'],
6227            '0x1531': ['DS', '1', 'DetectorSecondaryAngle'],
6228            '0x1600': ['CS', '1-3', 'ShutterShape'],
6229            '0x1602': ['IS', '1', 'ShutterLeftVerticalEdge'],
6230            '0x1604': ['IS', '1', 'ShutterRightVerticalEdge'],
6231            '0x1606': ['IS', '1', 'ShutterUpperHorizontalEdge'],
6232            '0x1608': ['IS', '1', 'ShutterLowerHorizontalEdge'],
6233            '0x1610': ['IS', '2', 'CenterOfCircularShutter'],
6234            '0x1612': ['IS', '1', 'RadiusOfCircularShutter'],
6235            '0x1620': ['IS', '2-2n', 'VerticesOfThePolygonalShutter'],
6236            '0x1622': ['US', '1', 'ShutterPresentationValue'],
6237            '0x1623': ['US', '1', 'ShutterOverlayGroup'],
6238            '0x1624': ['US', '3', 'ShutterPresentationColorCIELabValue'],
6239            '0x1700': ['CS', '1-3', 'CollimatorShape'],
6240            '0x1702': ['IS', '1', 'CollimatorLeftVerticalEdge'],
6241            '0x1704': ['IS', '1', 'CollimatorRightVerticalEdge'],
6242            '0x1706': ['IS', '1', 'CollimatorUpperHorizontalEdge'],
6243            '0x1708': ['IS', '1', 'CollimatorLowerHorizontalEdge'],
6244            '0x1710': ['IS', '2', 'CenterOfCircularCollimator'],
6245            '0x1712': ['IS', '1', 'RadiusOfCircularCollimator'],
6246            '0x1720': ['IS', '2-2n', 'VerticesOfThePolygonalCollimator'],
6247            '0x1800': ['CS', '1', 'AcquisitionTimeSynchronized'],
6248            '0x1801': ['SH', '1', 'TimeSource'],
6249            '0x1802': ['CS', '1', 'TimeDistributionProtocol'],
6250            '0x1803': ['LO', '1', 'NTPSourceAddress'],
6251            '0x2001': ['IS', '1-n', 'PageNumberVector'],
6252            '0x2002': ['SH', '1-n', 'FrameLabelVector'],
6253            '0x2003': ['DS', '1-n', 'FramePrimaryAngleVector'],
6254            '0x2004': ['DS', '1-n', 'FrameSecondaryAngleVector'],
6255            '0x2005': ['DS', '1-n', 'SliceLocationVector'],
6256            '0x2006': ['SH', '1-n', 'DisplayWindowLabelVector'],
6257            '0x2010': ['DS', '2', 'NominalScannedPixelSpacing'],
6258            '0x2020': ['CS', '1', 'DigitizingDeviceTransportDirection'],
6259            '0x2030': ['DS', '1', 'RotationOfScannedFilm'],
6260            '0x2041': ['SQ', '1', 'BiopsyTargetSequence'],
6261            '0x2042': ['UI', '1', 'TargetUID'],
6262            '0x2043': ['FL', '2', 'LocalizingCursorPosition'],
6263            '0x2044': ['FL', '3', 'CalculatedTargetPosition'],
6264            '0x2045': ['SH', '1', 'TargetLabel'],
6265            '0x2046': ['FL', '1', 'DisplayedZValue'],
6266            '0x3100': ['CS', '1', 'IVUSAcquisition'],
6267            '0x3101': ['DS', '1', 'IVUSPullbackRate'],
6268            '0x3102': ['DS', '1', 'IVUSGatedRate'],
6269            '0x3103': ['IS', '1', 'IVUSPullbackStartFrameNumber'],
6270            '0x3104': ['IS', '1', 'IVUSPullbackStopFrameNumber'],
6271            '0x3105': ['IS', '1-n', 'LesionNumber'],
6272            '0x4000': ['LT', '1', 'AcquisitionComments'],
6273            '0x5000': ['SH', '1-n', 'OutputPower'],
6274            '0x5010': ['LO', '1-n', 'TransducerData'],
6275            '0x5012': ['DS', '1', 'FocusDepth'],
6276            '0x5020': ['LO', '1', 'ProcessingFunction'],
6277            '0x5021': ['LO', '1', 'PostprocessingFunction'],
6278            '0x5022': ['DS', '1', 'MechanicalIndex'],
6279            '0x5024': ['DS', '1', 'BoneThermalIndex'],
6280            '0x5026': ['DS', '1', 'CranialThermalIndex'],
6281            '0x5027': ['DS', '1', 'SoftTissueThermalIndex'],
6282            '0x5028': ['DS', '1', 'SoftTissueFocusThermalIndex'],
6283            '0x5029': ['DS', '1', 'SoftTissueSurfaceThermalIndex'],
6284            '0x5030': ['DS', '1', 'DynamicRange'],
6285            '0x5040': ['DS', '1', 'TotalGain'],
6286            '0x5050': ['IS', '1', 'DepthOfScanField'],
6287            '0x5100': ['CS', '1', 'PatientPosition'],
6288            '0x5101': ['CS', '1', 'ViewPosition'],
6289            '0x5104': ['SQ', '1', 'ProjectionEponymousNameCodeSequence'],
6290            '0x5210': ['DS', '6', 'ImageTransformationMatrix'],
6291            '0x5212': ['DS', '3', 'ImageTranslationVector'],
6292            '0x6000': ['DS', '1', 'Sensitivity'],
6293            '0x6011': ['SQ', '1', 'SequenceOfUltrasoundRegions'],
6294            '0x6012': ['US', '1', 'RegionSpatialFormat'],
6295            '0x6014': ['US', '1', 'RegionDataType'],
6296            '0x6016': ['UL', '1', 'RegionFlags'],
6297            '0x6018': ['UL', '1', 'RegionLocationMinX0'],
6298            '0x601A': ['UL', '1', 'RegionLocationMinY0'],
6299            '0x601C': ['UL', '1', 'RegionLocationMaxX1'],
6300            '0x601E': ['UL', '1', 'RegionLocationMaxY1'],
6301            '0x6020': ['SL', '1', 'ReferencePixelX0'],
6302            '0x6022': ['SL', '1', 'ReferencePixelY0'],
6303            '0x6024': ['US', '1', 'PhysicalUnitsXDirection'],
6304            '0x6026': ['US', '1', 'PhysicalUnitsYDirection'],
6305            '0x6028': ['FD', '1', 'ReferencePixelPhysicalValueX'],
6306            '0x602A': ['FD', '1', 'ReferencePixelPhysicalValueY'],
6307            '0x602C': ['FD', '1', 'PhysicalDeltaX'],
6308            '0x602E': ['FD', '1', 'PhysicalDeltaY'],
6309            '0x6030': ['UL', '1', 'TransducerFrequency'],
6310            '0x6031': ['CS', '1', 'TransducerType'],
6311            '0x6032': ['UL', '1', 'PulseRepetitionFrequency'],
6312            '0x6034': ['FD', '1', 'DopplerCorrectionAngle'],
6313            '0x6036': ['FD', '1', 'SteeringAngle'],
6314            '0x6038': ['UL', '1', 'DopplerSampleVolumeXPositionRetired'],
6315            '0x6039': ['SL', '1', 'DopplerSampleVolumeXPosition'],
6316            '0x603A': ['UL', '1', 'DopplerSampleVolumeYPositionRetired'],
6317            '0x603B': ['SL', '1', 'DopplerSampleVolumeYPosition'],
6318            '0x603C': ['UL', '1', 'TMLinePositionX0Retired'],
6319            '0x603D': ['SL', '1', 'TMLinePositionX0'],
6320            '0x603E': ['UL', '1', 'TMLinePositionY0Retired'],
6321            '0x603F': ['SL', '1', 'TMLinePositionY0'],
6322            '0x6040': ['UL', '1', 'TMLinePositionX1Retired'],
6323            '0x6041': ['SL', '1', 'TMLinePositionX1'],
6324            '0x6042': ['UL', '1', 'TMLinePositionY1Retired'],
6325            '0x6043': ['SL', '1', 'TMLinePositionY1'],
6326            '0x6044': ['US', '1', 'PixelComponentOrganization'],
6327            '0x6046': ['UL', '1', 'PixelComponentMask'],
6328            '0x6048': ['UL', '1', 'PixelComponentRangeStart'],
6329            '0x604A': ['UL', '1', 'PixelComponentRangeStop'],
6330            '0x604C': ['US', '1', 'PixelComponentPhysicalUnits'],
6331            '0x604E': ['US', '1', 'PixelComponentDataType'],
6332            '0x6050': ['UL', '1', 'NumberOfTableBreakPoints'],
6333            '0x6052': ['UL', '1-n', 'TableOfXBreakPoints'],
6334            '0x6054': ['FD', '1-n', 'TableOfYBreakPoints'],
6335            '0x6056': ['UL', '1', 'NumberOfTableEntries'],
6336            '0x6058': ['UL', '1-n', 'TableOfPixelValues'],
6337            '0x605A': ['FL', '1-n', 'TableOfParameterValues'],
6338            '0x6060': ['FL', '1-n', 'RWaveTimeVector'],
6339            '0x7000': ['CS', '1', 'DetectorConditionsNominalFlag'],
6340            '0x7001': ['DS', '1', 'DetectorTemperature'],
6341            '0x7004': ['CS', '1', 'DetectorType'],
6342            '0x7005': ['CS', '1', 'DetectorConfiguration'],
6343            '0x7006': ['LT', '1', 'DetectorDescription'],
6344            '0x7008': ['LT', '1', 'DetectorMode'],
6345            '0x700A': ['SH', '1', 'DetectorID'],
6346            '0x700C': ['DA', '1', 'DateOfLastDetectorCalibration'],
6347            '0x700E': ['TM', '1', 'TimeOfLastDetectorCalibration'],
6348            '0x7010': ['IS', '1', 'ExposuresOnDetectorSinceLastCalibration'],
6349            '0x7011': ['IS', '1', 'ExposuresOnDetectorSinceManufactured'],
6350            '0x7012': ['DS', '1', 'DetectorTimeSinceLastExposure'],
6351            '0x7014': ['DS', '1', 'DetectorActiveTime'],
6352            '0x7016': ['DS', '1', 'DetectorActivationOffsetFromExposure'],
6353            '0x701A': ['DS', '2', 'DetectorBinning'],
6354            '0x7020': ['DS', '2', 'DetectorElementPhysicalSize'],
6355            '0x7022': ['DS', '2', 'DetectorElementSpacing'],
6356            '0x7024': ['CS', '1', 'DetectorActiveShape'],
6357            '0x7026': ['DS', '1-2', 'DetectorActiveDimensions'],
6358            '0x7028': ['DS', '2', 'DetectorActiveOrigin'],
6359            '0x702A': ['LO', '1', 'DetectorManufacturerName'],
6360            '0x702B': ['LO', '1', 'DetectorManufacturerModelName'],
6361            '0x7030': ['DS', '2', 'FieldOfViewOrigin'],
6362            '0x7032': ['DS', '1', 'FieldOfViewRotation'],
6363            '0x7034': ['CS', '1', 'FieldOfViewHorizontalFlip'],
6364            '0x7036': ['FL', '2', 'PixelDataAreaOriginRelativeToFOV'],
6365            '0x7038': ['FL', '1', 'PixelDataAreaRotationAngleRelativeToFOV'],
6366            '0x7040': ['LT', '1', 'GridAbsorbingMaterial'],
6367            '0x7041': ['LT', '1', 'GridSpacingMaterial'],
6368            '0x7042': ['DS', '1', 'GridThickness'],
6369            '0x7044': ['DS', '1', 'GridPitch'],
6370            '0x7046': ['IS', '2', 'GridAspectRatio'],
6371            '0x7048': ['DS', '1', 'GridPeriod'],
6372            '0x704C': ['DS', '1', 'GridFocalDistance'],
6373            '0x7050': ['CS', '1-n', 'FilterMaterial'],
6374            '0x7052': ['DS', '1-n', 'FilterThicknessMinimum'],
6375            '0x7054': ['DS', '1-n', 'FilterThicknessMaximum'],
6376            '0x7056': ['FL', '1-n', 'FilterBeamPathLengthMinimum'],
6377            '0x7058': ['FL', '1-n', 'FilterBeamPathLengthMaximum'],
6378            '0x7060': ['CS', '1', 'ExposureControlMode'],
6379            '0x7062': ['LT', '1', 'ExposureControlModeDescription'],
6380            '0x7064': ['CS', '1', 'ExposureStatus'],
6381            '0x7065': ['DS', '1', 'PhototimerSetting'],
6382            '0x8150': ['DS', '1', 'ExposureTimeInuS'],
6383            '0x8151': ['DS', '1', 'XRayTubeCurrentInuA'],
6384            '0x9004': ['CS', '1', 'ContentQualification'],
6385            '0x9005': ['SH', '1', 'PulseSequenceName'],
6386            '0x9006': ['SQ', '1', 'MRImagingModifierSequence'],
6387            '0x9008': ['CS', '1', 'EchoPulseSequence'],
6388            '0x9009': ['CS', '1', 'InversionRecovery'],
6389            '0x9010': ['CS', '1', 'FlowCompensation'],
6390            '0x9011': ['CS', '1', 'MultipleSpinEcho'],
6391            '0x9012': ['CS', '1', 'MultiPlanarExcitation'],
6392            '0x9014': ['CS', '1', 'PhaseContrast'],
6393            '0x9015': ['CS', '1', 'TimeOfFlightContrast'],
6394            '0x9016': ['CS', '1', 'Spoiling'],
6395            '0x9017': ['CS', '1', 'SteadyStatePulseSequence'],
6396            '0x9018': ['CS', '1', 'EchoPlanarPulseSequence'],
6397            '0x9019': ['FD', '1', 'TagAngleFirstAxis'],
6398            '0x9020': ['CS', '1', 'MagnetizationTransfer'],
6399            '0x9021': ['CS', '1', 'T2Preparation'],
6400            '0x9022': ['CS', '1', 'BloodSignalNulling'],
6401            '0x9024': ['CS', '1', 'SaturationRecovery'],
6402            '0x9025': ['CS', '1', 'SpectrallySelectedSuppression'],
6403            '0x9026': ['CS', '1', 'SpectrallySelectedExcitation'],
6404            '0x9027': ['CS', '1', 'SpatialPresaturation'],
6405            '0x9028': ['CS', '1', 'Tagging'],
6406            '0x9029': ['CS', '1', 'OversamplingPhase'],
6407            '0x9030': ['FD', '1', 'TagSpacingFirstDimension'],
6408            '0x9032': ['CS', '1', 'GeometryOfKSpaceTraversal'],
6409            '0x9033': ['CS', '1', 'SegmentedKSpaceTraversal'],
6410            '0x9034': ['CS', '1', 'RectilinearPhaseEncodeReordering'],
6411            '0x9035': ['FD', '1', 'TagThickness'],
6412            '0x9036': ['CS', '1', 'PartialFourierDirection'],
6413            '0x9037': ['CS', '1', 'CardiacSynchronizationTechnique'],
6414            '0x9041': ['LO', '1', 'ReceiveCoilManufacturerName'],
6415            '0x9042': ['SQ', '1', 'MRReceiveCoilSequence'],
6416            '0x9043': ['CS', '1', 'ReceiveCoilType'],
6417            '0x9044': ['CS', '1', 'QuadratureReceiveCoil'],
6418            '0x9045': ['SQ', '1', 'MultiCoilDefinitionSequence'],
6419            '0x9046': ['LO', '1', 'MultiCoilConfiguration'],
6420            '0x9047': ['SH', '1', 'MultiCoilElementName'],
6421            '0x9048': ['CS', '1', 'MultiCoilElementUsed'],
6422            '0x9049': ['SQ', '1', 'MRTransmitCoilSequence'],
6423            '0x9050': ['LO', '1', 'TransmitCoilManufacturerName'],
6424            '0x9051': ['CS', '1', 'TransmitCoilType'],
6425            '0x9052': ['FD', '1-2', 'SpectralWidth'],
6426            '0x9053': ['FD', '1-2', 'ChemicalShiftReference'],
6427            '0x9054': ['CS', '1', 'VolumeLocalizationTechnique'],
6428            '0x9058': ['US', '1', 'MRAcquisitionFrequencyEncodingSteps'],
6429            '0x9059': ['CS', '1', 'Decoupling'],
6430            '0x9060': ['CS', '1-2', 'DecoupledNucleus'],
6431            '0x9061': ['FD', '1-2', 'DecouplingFrequency'],
6432            '0x9062': ['CS', '1', 'DecouplingMethod'],
6433            '0x9063': ['FD', '1-2', 'DecouplingChemicalShiftReference'],
6434            '0x9064': ['CS', '1', 'KSpaceFiltering'],
6435            '0x9065': ['CS', '1-2', 'TimeDomainFiltering'],
6436            '0x9066': ['US', '1-2', 'NumberOfZeroFills'],
6437            '0x9067': ['CS', '1', 'BaselineCorrection'],
6438            '0x9069': ['FD', '1', 'ParallelReductionFactorInPlane'],
6439            '0x9070': ['FD', '1', 'CardiacRRIntervalSpecified'],
6440            '0x9073': ['FD', '1', 'AcquisitionDuration'],
6441            '0x9074': ['DT', '1', 'FrameAcquisitionDateTime'],
6442            '0x9075': ['CS', '1', 'DiffusionDirectionality'],
6443            '0x9076': ['SQ', '1', 'DiffusionGradientDirectionSequence'],
6444            '0x9077': ['CS', '1', 'ParallelAcquisition'],
6445            '0x9078': ['CS', '1', 'ParallelAcquisitionTechnique'],
6446            '0x9079': ['FD', '1-n', 'InversionTimes'],
6447            '0x9080': ['ST', '1', 'MetaboliteMapDescription'],
6448            '0x9081': ['CS', '1', 'PartialFourier'],
6449            '0x9082': ['FD', '1', 'EffectiveEchoTime'],
6450            '0x9083': ['SQ', '1', 'MetaboliteMapCodeSequence'],
6451            '0x9084': ['SQ', '1', 'ChemicalShiftSequence'],
6452            '0x9085': ['CS', '1', 'CardiacSignalSource'],
6453            '0x9087': ['FD', '1', 'DiffusionBValue'],
6454            '0x9089': ['FD', '3', 'DiffusionGradientOrientation'],
6455            '0x9090': ['FD', '3', 'VelocityEncodingDirection'],
6456            '0x9091': ['FD', '1', 'VelocityEncodingMinimumValue'],
6457            '0x9092': ['SQ', '1', 'VelocityEncodingAcquisitionSequence'],
6458            '0x9093': ['US', '1', 'NumberOfKSpaceTrajectories'],
6459            '0x9094': ['CS', '1', 'CoverageOfKSpace'],
6460            '0x9095': ['UL', '1', 'SpectroscopyAcquisitionPhaseRows'],
6461            '0x9096': ['FD', '1', 'ParallelReductionFactorInPlaneRetired'],
6462            '0x9098': ['FD', '1-2', 'TransmitterFrequency'],
6463            '0x9100': ['CS', '1-2', 'ResonantNucleus'],
6464            '0x9101': ['CS', '1', 'FrequencyCorrection'],
6465            '0x9103': ['SQ', '1', 'MRSpectroscopyFOVGeometrySequence'],
6466            '0x9104': ['FD', '1', 'SlabThickness'],
6467            '0x9105': ['FD', '3', 'SlabOrientation'],
6468            '0x9106': ['FD', '3', 'MidSlabPosition'],
6469            '0x9107': ['SQ', '1', 'MRSpatialSaturationSequence'],
6470            '0x9112': ['SQ', '1', 'MRTimingAndRelatedParametersSequence'],
6471            '0x9114': ['SQ', '1', 'MREchoSequence'],
6472            '0x9115': ['SQ', '1', 'MRModifierSequence'],
6473            '0x9117': ['SQ', '1', 'MRDiffusionSequence'],
6474            '0x9118': ['SQ', '1', 'CardiacSynchronizationSequence'],
6475            '0x9119': ['SQ', '1', 'MRAveragesSequence'],
6476            '0x9125': ['SQ', '1', 'MRFOVGeometrySequence'],
6477            '0x9126': ['SQ', '1', 'VolumeLocalizationSequence'],
6478            '0x9127': ['UL', '1', 'SpectroscopyAcquisitionDataColumns'],
6479            '0x9147': ['CS', '1', 'DiffusionAnisotropyType'],
6480            '0x9151': ['DT', '1', 'FrameReferenceDateTime'],
6481            '0x9152': ['SQ', '1', 'MRMetaboliteMapSequence'],
6482            '0x9155': ['FD', '1', 'ParallelReductionFactorOutOfPlane'],
6483            '0x9159': ['UL', '1', 'SpectroscopyAcquisitionOutOfPlanePhaseSteps'],
6484            '0x9166': ['CS', '1', 'BulkMotionStatus'],
6485            '0x9168': ['FD', '1', 'ParallelReductionFactorSecondInPlane'],
6486            '0x9169': ['CS', '1', 'CardiacBeatRejectionTechnique'],
6487            '0x9170': ['CS', '1', 'RespiratoryMotionCompensationTechnique'],
6488            '0x9171': ['CS', '1', 'RespiratorySignalSource'],
6489            '0x9172': ['CS', '1', 'BulkMotionCompensationTechnique'],
6490            '0x9173': ['CS', '1', 'BulkMotionSignalSource'],
6491            '0x9174': ['CS', '1', 'ApplicableSafetyStandardAgency'],
6492            '0x9175': ['LO', '1', 'ApplicableSafetyStandardDescription'],
6493            '0x9176': ['SQ', '1', 'OperatingModeSequence'],
6494            '0x9177': ['CS', '1', 'OperatingModeType'],
6495            '0x9178': ['CS', '1', 'OperatingMode'],
6496            '0x9179': ['CS', '1', 'SpecificAbsorptionRateDefinition'],
6497            '0x9180': ['CS', '1', 'GradientOutputType'],
6498            '0x9181': ['FD', '1', 'SpecificAbsorptionRateValue'],
6499            '0x9182': ['FD', '1', 'GradientOutput'],
6500            '0x9183': ['CS', '1', 'FlowCompensationDirection'],
6501            '0x9184': ['FD', '1', 'TaggingDelay'],
6502            '0x9185': ['ST', '1', 'RespiratoryMotionCompensationTechniqueDescription'],
6503            '0x9186': ['SH', '1', 'RespiratorySignalSourceID'],
6504            '0x9195': ['FD', '1', 'ChemicalShiftMinimumIntegrationLimitInHz'],
6505            '0x9196': ['FD', '1', 'ChemicalShiftMaximumIntegrationLimitInHz'],
6506            '0x9197': ['SQ', '1', 'MRVelocityEncodingSequence'],
6507            '0x9198': ['CS', '1', 'FirstOrderPhaseCorrection'],
6508            '0x9199': ['CS', '1', 'WaterReferencedPhaseCorrection'],
6509            '0x9200': ['CS', '1', 'MRSpectroscopyAcquisitionType'],
6510            '0x9214': ['CS', '1', 'RespiratoryCyclePosition'],
6511            '0x9217': ['FD', '1', 'VelocityEncodingMaximumValue'],
6512            '0x9218': ['FD', '1', 'TagSpacingSecondDimension'],
6513            '0x9219': ['SS', '1', 'TagAngleSecondAxis'],
6514            '0x9220': ['FD', '1', 'FrameAcquisitionDuration'],
6515            '0x9226': ['SQ', '1', 'MRImageFrameTypeSequence'],
6516            '0x9227': ['SQ', '1', 'MRSpectroscopyFrameTypeSequence'],
6517            '0x9231': ['US', '1', 'MRAcquisitionPhaseEncodingStepsInPlane'],
6518            '0x9232': ['US', '1', 'MRAcquisitionPhaseEncodingStepsOutOfPlane'],
6519            '0x9234': ['UL', '1', 'SpectroscopyAcquisitionPhaseColumns'],
6520            '0x9236': ['CS', '1', 'CardiacCyclePosition'],
6521            '0x9239': ['SQ', '1', 'SpecificAbsorptionRateSequence'],
6522            '0x9240': ['US', '1', 'RFEchoTrainLength'],
6523            '0x9241': ['US', '1', 'GradientEchoTrainLength'],
6524            '0x9250': ['CS', '1', 'ArterialSpinLabelingContrast'],
6525            '0x9251': ['SQ', '1', 'MRArterialSpinLabelingSequence'],
6526            '0x9252': ['LO', '1', 'ASLTechniqueDescription'],
6527            '0x9253': ['US', '1', 'ASLSlabNumber'],
6528            '0x9254': ['FD', '1', 'ASLSlabThickness'],
6529            '0x9255': ['FD', '3', 'ASLSlabOrientation'],
6530            '0x9256': ['FD', '3', 'ASLMidSlabPosition'],
6531            '0x9257': ['CS', '1', 'ASLContext'],
6532            '0x9258': ['UL', '1', 'ASLPulseTrainDuration'],
6533            '0x9259': ['CS', '1', 'ASLCrusherFlag'],
6534            '0x925A': ['FD', '1', 'ASLCrusherFlowLimit'],
6535            '0x925B': ['LO', '1', 'ASLCrusherDescription'],
6536            '0x925C': ['CS', '1', 'ASLBolusCutoffFlag'],
6537            '0x925D': ['SQ', '1', 'ASLBolusCutoffTimingSequence'],
6538            '0x925E': ['LO', '1', 'ASLBolusCutoffTechnique'],
6539            '0x925F': ['UL', '1', 'ASLBolusCutoffDelayTime'],
6540            '0x9260': ['SQ', '1', 'ASLSlabSequence'],
6541            '0x9295': ['FD', '1', 'ChemicalShiftMinimumIntegrationLimitInppm'],
6542            '0x9296': ['FD', '1', 'ChemicalShiftMaximumIntegrationLimitInppm'],
6543            '0x9297': ['CS', '1', 'WaterReferenceAcquisition'],
6544            '0x9298': ['IS', '1', 'EchoPeakPosition'],
6545            '0x9301': ['SQ', '1', 'CTAcquisitionTypeSequence'],
6546            '0x9302': ['CS', '1', 'AcquisitionType'],
6547            '0x9303': ['FD', '1', 'TubeAngle'],
6548            '0x9304': ['SQ', '1', 'CTAcquisitionDetailsSequence'],
6549            '0x9305': ['FD', '1', 'RevolutionTime'],
6550            '0x9306': ['FD', '1', 'SingleCollimationWidth'],
6551            '0x9307': ['FD', '1', 'TotalCollimationWidth'],
6552            '0x9308': ['SQ', '1', 'CTTableDynamicsSequence'],
6553            '0x9309': ['FD', '1', 'TableSpeed'],
6554            '0x9310': ['FD', '1', 'TableFeedPerRotation'],
6555            '0x9311': ['FD', '1', 'SpiralPitchFactor'],
6556            '0x9312': ['SQ', '1', 'CTGeometrySequence'],
6557            '0x9313': ['FD', '3', 'DataCollectionCenterPatient'],
6558            '0x9314': ['SQ', '1', 'CTReconstructionSequence'],
6559            '0x9315': ['CS', '1', 'ReconstructionAlgorithm'],
6560            '0x9316': ['CS', '1', 'ConvolutionKernelGroup'],
6561            '0x9317': ['FD', '2', 'ReconstructionFieldOfView'],
6562            '0x9318': ['FD', '3', 'ReconstructionTargetCenterPatient'],
6563            '0x9319': ['FD', '1', 'ReconstructionAngle'],
6564            '0x9320': ['SH', '1', 'ImageFilter'],
6565            '0x9321': ['SQ', '1', 'CTExposureSequence'],
6566            '0x9322': ['FD', '2', 'ReconstructionPixelSpacing'],
6567            '0x9323': ['CS', '1', 'ExposureModulationType'],
6568            '0x9324': ['FD', '1', 'EstimatedDoseSaving'],
6569            '0x9325': ['SQ', '1', 'CTXRayDetailsSequence'],
6570            '0x9326': ['SQ', '1', 'CTPositionSequence'],
6571            '0x9327': ['FD', '1', 'TablePosition'],
6572            '0x9328': ['FD', '1', 'ExposureTimeInms'],
6573            '0x9329': ['SQ', '1', 'CTImageFrameTypeSequence'],
6574            '0x9330': ['FD', '1', 'XRayTubeCurrentInmA'],
6575            '0x9332': ['FD', '1', 'ExposureInmAs'],
6576            '0x9333': ['CS', '1', 'ConstantVolumeFlag'],
6577            '0x9334': ['CS', '1', 'FluoroscopyFlag'],
6578            '0x9335': ['FD', '1', 'DistanceSourceToDataCollectionCenter'],
6579            '0x9337': ['US', '1', 'ContrastBolusAgentNumber'],
6580            '0x9338': ['SQ', '1', 'ContrastBolusIngredientCodeSequence'],
6581            '0x9340': ['SQ', '1', 'ContrastAdministrationProfileSequence'],
6582            '0x9341': ['SQ', '1', 'ContrastBolusUsageSequence'],
6583            '0x9342': ['CS', '1', 'ContrastBolusAgentAdministered'],
6584            '0x9343': ['CS', '1', 'ContrastBolusAgentDetected'],
6585            '0x9344': ['CS', '1', 'ContrastBolusAgentPhase'],
6586            '0x9345': ['FD', '1', 'CTDIvol'],
6587            '0x9346': ['SQ', '1', 'CTDIPhantomTypeCodeSequence'],
6588            '0x9351': ['FL', '1', 'CalciumScoringMassFactorPatient'],
6589            '0x9352': ['FL', '3', 'CalciumScoringMassFactorDevice'],
6590            '0x9353': ['FL', '1', 'EnergyWeightingFactor'],
6591            '0x9360': ['SQ', '1', 'CTAdditionalXRaySourceSequence'],
6592            '0x9401': ['SQ', '1', 'ProjectionPixelCalibrationSequence'],
6593            '0x9402': ['FL', '1', 'DistanceSourceToIsocenter'],
6594            '0x9403': ['FL', '1', 'DistanceObjectToTableTop'],
6595            '0x9404': ['FL', '2', 'ObjectPixelSpacingInCenterOfBeam'],
6596            '0x9405': ['SQ', '1', 'PositionerPositionSequence'],
6597            '0x9406': ['SQ', '1', 'TablePositionSequence'],
6598            '0x9407': ['SQ', '1', 'CollimatorShapeSequence'],
6599            '0x9410': ['CS', '1', 'PlanesInAcquisition'],
6600            '0x9412': ['SQ', '1', 'XAXRFFrameCharacteristicsSequence'],
6601            '0x9417': ['SQ', '1', 'FrameAcquisitionSequence'],
6602            '0x9420': ['CS', '1', 'XRayReceptorType'],
6603            '0x9423': ['LO', '1', 'AcquisitionProtocolName'],
6604            '0x9424': ['LT', '1', 'AcquisitionProtocolDescription'],
6605            '0x9425': ['CS', '1', 'ContrastBolusIngredientOpaque'],
6606            '0x9426': ['FL', '1', 'DistanceReceptorPlaneToDetectorHousing'],
6607            '0x9427': ['CS', '1', 'IntensifierActiveShape'],
6608            '0x9428': ['FL', '1-2', 'IntensifierActiveDimensions'],
6609            '0x9429': ['FL', '2', 'PhysicalDetectorSize'],
6610            '0x9430': ['FL', '2', 'PositionOfIsocenterProjection'],
6611            '0x9432': ['SQ', '1', 'FieldOfViewSequence'],
6612            '0x9433': ['LO', '1', 'FieldOfViewDescription'],
6613            '0x9434': ['SQ', '1', 'ExposureControlSensingRegionsSequence'],
6614            '0x9435': ['CS', '1', 'ExposureControlSensingRegionShape'],
6615            '0x9436': ['SS', '1', 'ExposureControlSensingRegionLeftVerticalEdge'],
6616            '0x9437': ['SS', '1', 'ExposureControlSensingRegionRightVerticalEdge'],
6617            '0x9438': ['SS', '1', 'ExposureControlSensingRegionUpperHorizontalEdge'],
6618            '0x9439': ['SS', '1', 'ExposureControlSensingRegionLowerHorizontalEdge'],
6619            '0x9440': ['SS', '2', 'CenterOfCircularExposureControlSensingRegion'],
6620            '0x9441': ['US', '1', 'RadiusOfCircularExposureControlSensingRegion'],
6621            '0x9442': ['SS', '2-n', 'VerticesOfThePolygonalExposureControlSensingRegion'],
6622            '0x9445': ['', '', ''],
6623            '0x9447': ['FL', '1', 'ColumnAngulationPatient'],
6624            '0x9449': ['FL', '1', 'BeamAngle'],
6625            '0x9451': ['SQ', '1', 'FrameDetectorParametersSequence'],
6626            '0x9452': ['FL', '1', 'CalculatedAnatomyThickness'],
6627            '0x9455': ['SQ', '1', 'CalibrationSequence'],
6628            '0x9456': ['SQ', '1', 'ObjectThicknessSequence'],
6629            '0x9457': ['CS', '1', 'PlaneIdentification'],
6630            '0x9461': ['FL', '1-2', 'FieldOfViewDimensionsInFloat'],
6631            '0x9462': ['SQ', '1', 'IsocenterReferenceSystemSequence'],
6632            '0x9463': ['FL', '1', 'PositionerIsocenterPrimaryAngle'],
6633            '0x9464': ['FL', '1', 'PositionerIsocenterSecondaryAngle'],
6634            '0x9465': ['FL', '1', 'PositionerIsocenterDetectorRotationAngle'],
6635            '0x9466': ['FL', '1', 'TableXPositionToIsocenter'],
6636            '0x9467': ['FL', '1', 'TableYPositionToIsocenter'],
6637            '0x9468': ['FL', '1', 'TableZPositionToIsocenter'],
6638            '0x9469': ['FL', '1', 'TableHorizontalRotationAngle'],
6639            '0x9470': ['FL', '1', 'TableHeadTiltAngle'],
6640            '0x9471': ['FL', '1', 'TableCradleTiltAngle'],
6641            '0x9472': ['SQ', '1', 'FrameDisplayShutterSequence'],
6642            '0x9473': ['FL', '1', 'AcquiredImageAreaDoseProduct'],
6643            '0x9474': ['CS', '1', 'CArmPositionerTabletopRelationship'],
6644            '0x9476': ['SQ', '1', 'XRayGeometrySequence'],
6645            '0x9477': ['SQ', '1', 'IrradiationEventIdentificationSequence'],
6646            '0x9504': ['SQ', '1', 'XRay3DFrameTypeSequence'],
6647            '0x9506': ['SQ', '1', 'ContributingSourcesSequence'],
6648            '0x9507': ['SQ', '1', 'XRay3DAcquisitionSequence'],
6649            '0x9508': ['FL', '1', 'PrimaryPositionerScanArc'],
6650            '0x9509': ['FL', '1', 'SecondaryPositionerScanArc'],
6651            '0x9510': ['FL', '1', 'PrimaryPositionerScanStartAngle'],
6652            '0x9511': ['FL', '1', 'SecondaryPositionerScanStartAngle'],
6653            '0x9514': ['FL', '1', 'PrimaryPositionerIncrement'],
6654            '0x9515': ['FL', '1', 'SecondaryPositionerIncrement'],
6655            '0x9516': ['DT', '1', 'StartAcquisitionDateTime'],
6656            '0x9517': ['DT', '1', 'EndAcquisitionDateTime'],
6657            '0x9518': ['SS', '1', 'PrimaryPositionerIncrementSign'],
6658            '0x9519': ['SS', '1', 'SecondaryPositionerIncrementSign'],
6659            '0x9524': ['LO', '1', 'ApplicationName'],
6660            '0x9525': ['LO', '1', 'ApplicationVersion'],
6661            '0x9526': ['LO', '1', 'ApplicationManufacturer'],
6662            '0x9527': ['CS', '1', 'AlgorithmType'],
6663            '0x9528': ['LO', '1', 'AlgorithmDescription'],
6664            '0x9530': ['SQ', '1', 'XRay3DReconstructionSequence'],
6665            '0x9531': ['LO', '1', 'ReconstructionDescription'],
6666            '0x9538': ['SQ', '1', 'PerProjectionAcquisitionSequence'],
6667            '0x9541': ['SQ', '1', 'DetectorPositionSequence'],
6668            '0x9542': ['SQ', '1', 'XRayAcquisitionDoseSequence'],
6669            '0x9543': ['FD', '1', 'XRaySourceIsocenterPrimaryAngle'],
6670            '0x9544': ['FD', '1', 'XRaySourceIsocenterSecondaryAngle'],
6671            '0x9545': ['FD', '1', 'BreastSupportIsocenterPrimaryAngle'],
6672            '0x9546': ['FD', '1', 'BreastSupportIsocenterSecondaryAngle'],
6673            '0x9547': ['FD', '1', 'BreastSupportXPositionToIsocenter'],
6674            '0x9548': ['FD', '1', 'BreastSupportYPositionToIsocenter'],
6675            '0x9549': ['FD', '1', 'BreastSupportZPositionToIsocenter'],
6676            '0x9550': ['FD', '1', 'DetectorIsocenterPrimaryAngle'],
6677            '0x9551': ['FD', '1', 'DetectorIsocenterSecondaryAngle'],
6678            '0x9552': ['FD', '1', 'DetectorXPositionToIsocenter'],
6679            '0x9553': ['FD', '1', 'DetectorYPositionToIsocenter'],
6680            '0x9554': ['FD', '1', 'DetectorZPositionToIsocenter'],
6681            '0x9555': ['SQ', '1', 'XRayGridSequence'],
6682            '0x9556': ['SQ', '1', 'XRayFilterSequence'],
6683            '0x9557': ['FD', '3', 'DetectorActiveAreaTLHCPosition'],
6684            '0x9558': ['FD', '6', 'DetectorActiveAreaOrientation'],
6685            '0x9559': ['CS', '1', 'PositionerPrimaryAngleDirection'],
6686            '0x9601': ['SQ', '1', 'DiffusionBMatrixSequence'],
6687            '0x9602': ['FD', '1', 'DiffusionBValueXX'],
6688            '0x9603': ['FD', '1', 'DiffusionBValueXY'],
6689            '0x9604': ['FD', '1', 'DiffusionBValueXZ'],
6690            '0x9605': ['FD', '1', 'DiffusionBValueYY'],
6691            '0x9606': ['FD', '1', 'DiffusionBValueYZ'],
6692            '0x9607': ['FD', '1', 'DiffusionBValueZZ'],
6693            '0x9701': ['DT', '1', 'DecayCorrectionDateTime'],
6694            '0x9715': ['FD', '1', 'StartDensityThreshold'],
6695            '0x9716': ['FD', '1', 'StartRelativeDensityDifferenceThreshold'],
6696            '0x9717': ['FD', '1', 'StartCardiacTriggerCountThreshold'],
6697            '0x9718': ['FD', '1', 'StartRespiratoryTriggerCountThreshold'],
6698            '0x9719': ['FD', '1', 'TerminationCountsThreshold'],
6699            '0x9720': ['FD', '1', 'TerminationDensityThreshold'],
6700            '0x9721': ['FD', '1', 'TerminationRelativeDensityThreshold'],
6701            '0x9722': ['FD', '1', 'TerminationTimeThreshold'],
6702            '0x9723': ['FD', '1', 'TerminationCardiacTriggerCountThreshold'],
6703            '0x9724': ['FD', '1', 'TerminationRespiratoryTriggerCountThreshold'],
6704            '0x9725': ['CS', '1', 'DetectorGeometry'],
6705            '0x9726': ['FD', '1', 'TransverseDetectorSeparation'],
6706            '0x9727': ['FD', '1', 'AxialDetectorDimension'],
6707            '0x9729': ['US', '1', 'RadiopharmaceuticalAgentNumber'],
6708            '0x9732': ['SQ', '1', 'PETFrameAcquisitionSequence'],
6709            '0x9733': ['SQ', '1', 'PETDetectorMotionDetailsSequence'],
6710            '0x9734': ['SQ', '1', 'PETTableDynamicsSequence'],
6711            '0x9735': ['SQ', '1', 'PETPositionSequence'],
6712            '0x9736': ['SQ', '1', 'PETFrameCorrectionFactorsSequence'],
6713            '0x9737': ['SQ', '1', 'RadiopharmaceuticalUsageSequence'],
6714            '0x9738': ['CS', '1', 'AttenuationCorrectionSource'],
6715            '0x9739': ['US', '1', 'NumberOfIterations'],
6716            '0x9740': ['US', '1', 'NumberOfSubsets'],
6717            '0x9749': ['SQ', '1', 'PETReconstructionSequence'],
6718            '0x9751': ['SQ', '1', 'PETFrameTypeSequence'],
6719            '0x9755': ['CS', '1', 'TimeOfFlightInformationUsed'],
6720            '0x9756': ['CS', '1', 'ReconstructionType'],
6721            '0x9758': ['CS', '1', 'DecayCorrected'],
6722            '0x9759': ['CS', '1', 'AttenuationCorrected'],
6723            '0x9760': ['CS', '1', 'ScatterCorrected'],
6724            '0x9761': ['CS', '1', 'DeadTimeCorrected'],
6725            '0x9762': ['CS', '1', 'GantryMotionCorrected'],
6726            '0x9763': ['CS', '1', 'PatientMotionCorrected'],
6727            '0x9764': ['CS', '1', 'CountLossNormalizationCorrected'],
6728            '0x9765': ['CS', '1', 'RandomsCorrected'],
6729            '0x9766': ['CS', '1', 'NonUniformRadialSamplingCorrected'],
6730            '0x9767': ['CS', '1', 'SensitivityCalibrated'],
6731            '0x9768': ['CS', '1', 'DetectorNormalizationCorrection'],
6732            '0x9769': ['CS', '1', 'IterativeReconstructionMethod'],
6733            '0x9770': ['CS', '1', 'AttenuationCorrectionTemporalRelationship'],
6734            '0x9771': ['SQ', '1', 'PatientPhysiologicalStateSequence'],
6735            '0x9772': ['SQ', '1', 'PatientPhysiologicalStateCodeSequence'],
6736            '0x9801': ['FD', '1-n', 'DepthsOfFocus'],
6737            '0x9803': ['SQ', '1', 'ExcludedIntervalsSequence'],
6738            '0x9804': ['DT', '1', 'ExclusionStartDateTime'],
6739            '0x9805': ['FD', '1', 'ExclusionDuration'],
6740            '0x9806': ['SQ', '1', 'USImageDescriptionSequence'],
6741            '0x9807': ['SQ', '1', 'ImageDataTypeSequence'],
6742            '0x9808': ['CS', '1', 'DataType'],
6743            '0x9809': ['SQ', '1', 'TransducerScanPatternCodeSequence'],
6744            '0x980B': ['CS', '1', 'AliasedDataType'],
6745            '0x980C': ['CS', '1', 'PositionMeasuringDeviceUsed'],
6746            '0x980D': ['SQ', '1', 'TransducerGeometryCodeSequence'],
6747            '0x980E': ['SQ', '1', 'TransducerBeamSteeringCodeSequence'],
6748            '0x980F': ['SQ', '1', 'TransducerApplicationCodeSequence'],
6749            '0x9810': ['xs', '1', 'ZeroVelocityPixelValue'],
6750            '0xA001': ['SQ', '1', 'ContributingEquipmentSequence'],
6751            '0xA002': ['DT', '1', 'ContributionDateTime'],
6752            '0xA003': ['ST', '1', 'ContributionDescription']
6753        },
6754        '0x0020': {
6755            '0x0000': ['UL', '1', 'GenericGroupLength'],
6756            '0x000D': ['UI', '1', 'StudyInstanceUID'],
6757            '0x000E': ['UI', '1', 'SeriesInstanceUID'],
6758            '0x0010': ['SH', '1', 'StudyID'],
6759            '0x0011': ['IS', '1', 'SeriesNumber'],
6760            '0x0012': ['IS', '1', 'AcquisitionNumber'],
6761            '0x0013': ['IS', '1', 'InstanceNumber'],
6762            '0x0014': ['IS', '1', 'IsotopeNumber'],
6763            '0x0015': ['IS', '1', 'PhaseNumber'],
6764            '0x0016': ['IS', '1', 'IntervalNumber'],
6765            '0x0017': ['IS', '1', 'TimeSlotNumber'],
6766            '0x0018': ['IS', '1', 'AngleNumber'],
6767            '0x0019': ['IS', '1', 'ItemNumber'],
6768            '0x0020': ['CS', '2', 'PatientOrientation'],
6769            '0x0022': ['IS', '1', 'OverlayNumber'],
6770            '0x0024': ['IS', '1', 'CurveNumber'],
6771            '0x0026': ['IS', '1', 'LUTNumber'],
6772            '0x0030': ['DS', '3', 'ImagePosition'],
6773            '0x0032': ['DS', '3', 'ImagePositionPatient'],
6774            '0x0035': ['DS', '6', 'ImageOrientation'],
6775            '0x0037': ['DS', '6', 'ImageOrientationPatient'],
6776            '0x0050': ['DS', '1', 'Location'],
6777            '0x0052': ['UI', '1', 'FrameOfReferenceUID'],
6778            '0x0060': ['CS', '1', 'Laterality'],
6779            '0x0062': ['CS', '1', 'ImageLaterality'],
6780            '0x0070': ['LO', '1', 'ImageGeometryType'],
6781            '0x0080': ['CS', '1-n', 'MaskingImage'],
6782            '0x00AA': ['IS', '1', 'ReportNumber'],
6783            '0x0100': ['IS', '1', 'TemporalPositionIdentifier'],
6784            '0x0105': ['IS', '1', 'NumberOfTemporalPositions'],
6785            '0x0110': ['DS', '1', 'TemporalResolution'],
6786            '0x0200': ['UI', '1', 'SynchronizationFrameOfReferenceUID'],
6787            '0x0242': ['UI', '1', 'SOPInstanceUIDOfConcatenationSource'],
6788            '0x1000': ['IS', '1', 'SeriesInStudy'],
6789            '0x1001': ['IS', '1', 'AcquisitionsInSeries'],
6790            '0x1002': ['IS', '1', 'ImagesInAcquisition'],
6791            '0x1003': ['IS', '1', 'ImagesInSeries'],
6792            '0x1004': ['IS', '1', 'AcquisitionsInStudy'],
6793            '0x1005': ['IS', '1', 'ImagesInStudy'],
6794            '0x1020': ['LO', '1-n', 'Reference'],
6795            '0x1040': ['LO', '1', 'PositionReferenceIndicator'],
6796            '0x1041': ['DS', '1', 'SliceLocation'],
6797            '0x1070': ['IS', '1-n', 'OtherStudyNumbers'],
6798            '0x1200': ['IS', '1', 'NumberOfPatientRelatedStudies'],
6799            '0x1202': ['IS', '1', 'NumberOfPatientRelatedSeries'],
6800            '0x1204': ['IS', '1', 'NumberOfPatientRelatedInstances'],
6801            '0x1206': ['IS', '1', 'NumberOfStudyRelatedSeries'],
6802            '0x1208': ['IS', '1', 'NumberOfStudyRelatedInstances'],
6803            '0x1209': ['IS', '1', 'NumberOfSeriesRelatedInstances'],
6804            '0x3100': ['CS', '1-n', 'SourceImageIDs'],
6805            '0x3401': ['CS', '1', 'ModifyingDeviceID'],
6806            '0x3402': ['CS', '1', 'ModifiedImageID'],
6807            '0x3403': ['DA', '1', 'ModifiedImageDate'],
6808            '0x3404': ['LO', '1', 'ModifyingDeviceManufacturer'],
6809            '0x3405': ['TM', '1', 'ModifiedImageTime'],
6810            '0x3406': ['LO', '1', 'ModifiedImageDescription'],
6811            '0x4000': ['LT', '1', 'ImageComments'],
6812            '0x5000': ['AT', '1-n', 'OriginalImageIdentification'],
6813            '0x5002': ['LO', '1-n', 'OriginalImageIdentificationNomenclature'],
6814            '0x9056': ['SH', '1', 'StackID'],
6815            '0x9057': ['UL', '1', 'InStackPositionNumber'],
6816            '0x9071': ['SQ', '1', 'FrameAnatomySequence'],
6817            '0x9072': ['CS', '1', 'FrameLaterality'],
6818            '0x9111': ['SQ', '1', 'FrameContentSequence'],
6819            '0x9113': ['SQ', '1', 'PlanePositionSequence'],
6820            '0x9116': ['SQ', '1', 'PlaneOrientationSequence'],
6821            '0x9128': ['UL', '1', 'TemporalPositionIndex'],
6822            '0x9153': ['FD', '1', 'NominalCardiacTriggerDelayTime'],
6823            '0x9154': ['FL', '1', 'NominalCardiacTriggerTimePriorToRPeak'],
6824            '0x9155': ['FL', '1', 'ActualCardiacTriggerTimePriorToRPeak'],
6825            '0x9156': ['US', '1', 'FrameAcquisitionNumber'],
6826            '0x9157': ['UL', '1-n', 'DimensionIndexValues'],
6827            '0x9158': ['LT', '1', 'FrameComments'],
6828            '0x9161': ['UI', '1', 'ConcatenationUID'],
6829            '0x9162': ['US', '1', 'InConcatenationNumber'],
6830            '0x9163': ['US', '1', 'InConcatenationTotalNumber'],
6831            '0x9164': ['UI', '1', 'DimensionOrganizationUID'],
6832            '0x9165': ['AT', '1', 'DimensionIndexPointer'],
6833            '0x9167': ['AT', '1', 'FunctionalGroupPointer'],
6834            '0x9170': ['SQ', '1', 'UnassignedSharedConvertedAttributesSequence'],
6835            '0x9171': ['SQ', '1', 'UnassignedPerFrameConvertedAttributesSequence'],
6836            '0x9172': ['SQ', '1', 'ConversionSourceAttributesSequence'],
6837            '0x9213': ['LO', '1', 'DimensionIndexPrivateCreator'],
6838            '0x9221': ['SQ', '1', 'DimensionOrganizationSequence'],
6839            '0x9222': ['SQ', '1', 'DimensionIndexSequence'],
6840            '0x9228': ['UL', '1', 'ConcatenationFrameOffsetNumber'],
6841            '0x9238': ['LO', '1', 'FunctionalGroupPrivateCreator'],
6842            '0x9241': ['FL', '1', 'NominalPercentageOfCardiacPhase'],
6843            '0x9245': ['FL', '1', 'NominalPercentageOfRespiratoryPhase'],
6844            '0x9246': ['FL', '1', 'StartingRespiratoryAmplitude'],
6845            '0x9247': ['CS', '1', 'StartingRespiratoryPhase'],
6846            '0x9248': ['FL', '1', 'EndingRespiratoryAmplitude'],
6847            '0x9249': ['CS', '1', 'EndingRespiratoryPhase'],
6848            '0x9250': ['CS', '1', 'RespiratoryTriggerType'],
6849            '0x9251': ['FD', '1', 'RRIntervalTimeNominal'],
6850            '0x9252': ['FD', '1', 'ActualCardiacTriggerDelayTime'],
6851            '0x9253': ['SQ', '1', 'RespiratorySynchronizationSequence'],
6852            '0x9254': ['FD', '1', 'RespiratoryIntervalTime'],
6853            '0x9255': ['FD', '1', 'NominalRespiratoryTriggerDelayTime'],
6854            '0x9256': ['FD', '1', 'RespiratoryTriggerDelayThreshold'],
6855            '0x9257': ['FD', '1', 'ActualRespiratoryTriggerDelayTime'],
6856            '0x9301': ['FD', '3', 'ImagePositionVolume'],
6857            '0x9302': ['FD', '6', 'ImageOrientationVolume'],
6858            '0x9307': ['CS', '1', 'UltrasoundAcquisitionGeometry'],
6859            '0x9308': ['FD', '3', 'ApexPosition'],
6860            '0x9309': ['FD', '16', 'VolumeToTransducerMappingMatrix'],
6861            '0x930A': ['FD', '16', 'VolumeToTableMappingMatrix'],
6862            '0x930B': ['CS', '1', 'VolumeToTransducerRelationship'],
6863            '0x930C': ['CS', '1', 'PatientFrameOfReferenceSource'],
6864            '0x930D': ['FD', '1', 'TemporalPositionTimeOffset'],
6865            '0x930E': ['SQ', '1', 'PlanePositionVolumeSequence'],
6866            '0x930F': ['SQ', '1', 'PlaneOrientationVolumeSequence'],
6867            '0x9310': ['SQ', '1', 'TemporalPositionSequence'],
6868            '0x9311': ['CS', '1', 'DimensionOrganizationType'],
6869            '0x9312': ['UI', '1', 'VolumeFrameOfReferenceUID'],
6870            '0x9313': ['UI', '1', 'TableFrameOfReferenceUID'],
6871            '0x9421': ['LO', '1', 'DimensionDescriptionLabel'],
6872            '0x9450': ['SQ', '1', 'PatientOrientationInFrameSequence'],
6873            '0x9453': ['LO', '1', 'FrameLabel'],
6874            '0x9518': ['US', '1-n', 'AcquisitionIndex'],
6875            '0x9529': ['SQ', '1', 'ContributingSOPInstancesReferenceSequence'],
6876            '0x9536': ['US', '1', 'ReconstructionIndex']
6877        },
6878        '0x0022': {
6879            '0x0000': ['UL', '1', 'GenericGroupLength'],
6880            '0x0001': ['US', '1', 'LightPathFilterPassThroughWavelength'],
6881            '0x0002': ['US', '2', 'LightPathFilterPassBand'],
6882            '0x0003': ['US', '1', 'ImagePathFilterPassThroughWavelength'],
6883            '0x0004': ['US', '2', 'ImagePathFilterPassBand'],
6884            '0x0005': ['CS', '1', 'PatientEyeMovementCommanded'],
6885            '0x0006': ['SQ', '1', 'PatientEyeMovementCommandCodeSequence'],
6886            '0x0007': ['FL', '1', 'SphericalLensPower'],
6887            '0x0008': ['FL', '1', 'CylinderLensPower'],
6888            '0x0009': ['FL', '1', 'CylinderAxis'],
6889            '0x000A': ['FL', '1', 'EmmetropicMagnification'],
6890            '0x000B': ['FL', '1', 'IntraOcularPressure'],
6891            '0x000C': ['FL', '1', 'HorizontalFieldOfView'],
6892            '0x000D': ['CS', '1', 'PupilDilated'],
6893            '0x000E': ['FL', '1', 'DegreeOfDilation'],
6894            '0x0010': ['FL', '1', 'StereoBaselineAngle'],
6895            '0x0011': ['FL', '1', 'StereoBaselineDisplacement'],
6896            '0x0012': ['FL', '1', 'StereoHorizontalPixelOffset'],
6897            '0x0013': ['FL', '1', 'StereoVerticalPixelOffset'],
6898            '0x0014': ['FL', '1', 'StereoRotation'],
6899            '0x0015': ['SQ', '1', 'AcquisitionDeviceTypeCodeSequence'],
6900            '0x0016': ['SQ', '1', 'IlluminationTypeCodeSequence'],
6901            '0x0017': ['SQ', '1', 'LightPathFilterTypeStackCodeSequence'],
6902            '0x0018': ['SQ', '1', 'ImagePathFilterTypeStackCodeSequence'],
6903            '0x0019': ['SQ', '1', 'LensesCodeSequence'],
6904            '0x001A': ['SQ', '1', 'ChannelDescriptionCodeSequence'],
6905            '0x001B': ['SQ', '1', 'RefractiveStateSequence'],
6906            '0x001C': ['SQ', '1', 'MydriaticAgentCodeSequence'],
6907            '0x001D': ['SQ', '1', 'RelativeImagePositionCodeSequence'],
6908            '0x001E': ['FL', '1', 'CameraAngleOfView'],
6909            '0x0020': ['SQ', '1', 'StereoPairsSequence'],
6910            '0x0021': ['SQ', '1', 'LeftImageSequence'],
6911            '0x0022': ['SQ', '1', 'RightImageSequence'],
6912            '0x0028': ['CS', '1', 'StereoPairsPresent'],
6913            '0x0030': ['FL', '1', 'AxialLengthOfTheEye'],
6914            '0x0031': ['SQ', '1', 'OphthalmicFrameLocationSequence'],
6915            '0x0032': ['FL', '2-2n', 'ReferenceCoordinates'],
6916            '0x0035': ['FL', '1', 'DepthSpatialResolution'],
6917            '0x0036': ['FL', '1', 'MaximumDepthDistortion'],
6918            '0x0037': ['FL', '1', 'AlongScanSpatialResolution'],
6919            '0x0038': ['FL', '1', 'MaximumAlongScanDistortion'],
6920            '0x0039': ['CS', '1', 'OphthalmicImageOrientation'],
6921            '0x0041': ['FL', '1', 'DepthOfTransverseImage'],
6922            '0x0042': ['SQ', '1', 'MydriaticAgentConcentrationUnitsSequence'],
6923            '0x0048': ['FL', '1', 'AcrossScanSpatialResolution'],
6924            '0x0049': ['FL', '1', 'MaximumAcrossScanDistortion'],
6925            '0x004E': ['DS', '1', 'MydriaticAgentConcentration'],
6926            '0x0055': ['FL', '1', 'IlluminationWaveLength'],
6927            '0x0056': ['FL', '1', 'IlluminationPower'],
6928            '0x0057': ['FL', '1', 'IlluminationBandwidth'],
6929            '0x0058': ['SQ', '1', 'MydriaticAgentSequence'],
6930            '0x1007': ['SQ', '1', 'OphthalmicAxialMeasurementsRightEyeSequence'],
6931            '0x1008': ['SQ', '1', 'OphthalmicAxialMeasurementsLeftEyeSequence'],
6932            '0x1009': ['CS', '1', 'OphthalmicAxialMeasurementsDeviceType'],
6933            '0x1010': ['CS', '1', 'OphthalmicAxialLengthMeasurementsType'],
6934            '0x1012': ['SQ', '1', 'OphthalmicAxialLengthSequence'],
6935            '0x1019': ['FL', '1', 'OphthalmicAxialLength'],
6936            '0x1024': ['SQ', '1', 'LensStatusCodeSequence'],
6937            '0x1025': ['SQ', '1', 'VitreousStatusCodeSequence'],
6938            '0x1028': ['SQ', '1', 'IOLFormulaCodeSequence'],
6939            '0x1029': ['LO', '1', 'IOLFormulaDetail'],
6940            '0x1033': ['FL', '1', 'KeratometerIndex'],
6941            '0x1035': ['SQ', '1', 'SourceOfOphthalmicAxialLengthCodeSequence'],
6942            '0x1037': ['FL', '1', 'TargetRefraction'],
6943            '0x1039': ['CS', '1', 'RefractiveProcedureOccurred'],
6944            '0x1040': ['SQ', '1', 'RefractiveSurgeryTypeCodeSequence'],
6945            '0x1044': ['SQ', '1', 'OphthalmicUltrasoundMethodCodeSequence'],
6946            '0x1050': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSequence'],
6947            '0x1053': ['FL', '1', 'IOLPower'],
6948            '0x1054': ['FL', '1', 'PredictedRefractiveError'],
6949            '0x1059': ['FL', '1', 'OphthalmicAxialLengthVelocity'],
6950            '0x1065': ['LO', '1', 'LensStatusDescription'],
6951            '0x1066': ['LO', '1', 'VitreousStatusDescription'],
6952            '0x1090': ['SQ', '1', 'IOLPowerSequence'],
6953            '0x1092': ['SQ', '1', 'LensConstantSequence'],
6954            '0x1093': ['LO', '1', 'IOLManufacturer'],
6955            '0x1094': ['LO', '1', 'LensConstantDescription'],
6956            '0x1095': ['LO', '1', 'ImplantName'],
6957            '0x1096': ['SQ', '1', 'KeratometryMeasurementTypeCodeSequence'],
6958            '0x1097': ['LO', '1', 'ImplantPartNumber'],
6959            '0x1100': ['SQ', '1', 'ReferencedOphthalmicAxialMeasurementsSequence'],
6960            '0x1101': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSegmentNameCodeSequence'],
6961            '0x1103': ['SQ', '1', 'RefractiveErrorBeforeRefractiveSurgeryCodeSequence'],
6962            '0x1121': ['FL', '1', 'IOLPowerForExactEmmetropia'],
6963            '0x1122': ['FL', '1', 'IOLPowerForExactTargetRefraction'],
6964            '0x1125': ['SQ', '1', 'AnteriorChamberDepthDefinitionCodeSequence'],
6965            '0x1127': ['SQ', '1', 'LensThicknessSequence'],
6966            '0x1128': ['SQ', '1', 'AnteriorChamberDepthSequence'],
6967            '0x1130': ['FL', '1', 'LensThickness'],
6968            '0x1131': ['FL', '1', 'AnteriorChamberDept
6968h'],
6969            '0x1132': ['SQ', '1', 'SourceOfLensThicknessDataCodeSequence'],
6970            '0x1133': ['SQ', '1', 'SourceOfAnteriorChamberDepthDataCodeSequence'],
6971            '0x1134': ['SQ', '1', 'SourceOfRefractiveMeasurementsSequence'],
6972            '0x1135': ['SQ', '1', 'SourceOfRefractiveMeasurementsCodeSequence'],
6973            '0x1140': ['CS', '1', 'OphthalmicAxialLengthMeasurementModified'],
6974            '0x1150': ['SQ', '1', 'OphthalmicAxialLengthDataSourceCodeSequence'],
6975            '0x1153': ['SQ', '1', 'OphthalmicAxialLengthAcquisitionMethodCodeSequence'],
6976            '0x1155': ['FL', '1', 'SignalToNoiseRatio'],
6977            '0x1159': ['LO', '1', 'OphthalmicAxialLengthDataSourceDescription'],
6978            '0x1210': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsTotalLengthSequence'],
6979            '0x1211': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsSegmentalLengthSequence'],
6980            '0x1212': ['SQ', '1', 'OphthalmicAxialLengthMeasurementsLengthSummationSequence'],
6981            '0x1220': ['SQ', '1', 'UltrasoundOphthalmicAxialLengthMeasurementsSequence'],
6982            '0x1225': ['SQ', '1', 'OpticalOphthalmicAxialLengthMeasurementsSequence'],
6983            '0x1230': ['SQ', '1', 'UltrasoundSelectedOphthalmicAxialLengthSequence'],
6984            '0x1250': ['SQ', '1', 'OphthalmicAxialLengthSelectionMethodCodeSequence'],
6985            '0x1255': ['SQ', '1', 'OpticalSelectedOphthalmicAxialLengthSequence'],
6986            '0x1257': ['SQ', '1', 'SelectedSegmentalOphthalmicAxialLengthSequence'],
6987            '0x1260': ['SQ', '1', 'SelectedTotalOphthalmicAxialLengthSequence'],
6988            '0x1262': ['SQ', '1', 'OphthalmicAxialLengthQualityMetricSequence'],
6989            '0x1265': ['SQ', '1', 'OphthalmicAxialLengthQualityMetricTypeCodeSequence'],
6990            '0x1273': ['LO', '1', 'OphthalmicAxialLengthQualityMetricTypeDescription'],
6991            '0x1300': ['SQ', '1', 'IntraocularLensCalculationsRightEyeSequence'],
6992            '0x1310': ['SQ', '1', 'IntraocularLensCalculationsLeftEyeSequence'],
6993            '0x1330': ['SQ', '1', 'ReferencedOphthalmicAxialLengthMeasurementQCImageSequence'],
6994            '0x1415': ['CS', '1', 'OphthalmicMappingDeviceType'],
6995            '0x1420': ['SQ', '1', 'AcquisitionMethodCodeSequence'],
6996            '0x1423': ['SQ', '1', 'AcquisitionMethodAlgorithmSequence'],
6997            '0x1436': ['SQ', '1', 'OphthalmicThicknessMapTypeCodeSequence'],
6998            '0x1443': ['SQ', '1', 'OphthalmicThicknessMappingNormalsSequence'],
6999            '0x1445': ['SQ', '1', 'RetinalThicknessDefinitionCodeSequence'],
7000            '0x1450': ['SQ', '1', 'PixelValueMappingToCodedConceptSequence'],
7001            '0x1452': ['xs', '1', 'MappedPixelValue'],
7002            '0x1454': ['LO', '1', 'PixelValueMappingExplanation'],
7003            '0x1458': ['SQ', '1', 'OphthalmicThicknessMapQualityThresholdSequence'],
7004            '0x1460': ['FL', '1', 'OphthalmicThicknessMapThresholdQualityRating'],
7005            '0x1463': ['FL', '2', 'AnatomicStructureReferencePoint'],
7006            '0x1465': ['SQ', '1', 'RegistrationToLocalizerSequence'],
7007            '0x1466': ['CS', '1', 'RegisteredLocalizerUnits'],
7008            '0x1467': ['FL', '2', 'RegisteredLocalizerTopLeftHandCorner'],
7009            '0x1468': ['FL', '2', 'RegisteredLocalizerBottomRightHandCorner'],
7010            '0x1470': ['SQ', '1', 'OphthalmicThicknessMapQualityRatingSequence'],
7011            '0x1472': ['SQ', '1', 'RelevantOPTAttributesSequence'],
7012            '0x1512': ['SQ', '1', 'TransformationMethodCodeSequence'],
7013            '0x1513': ['SQ', '1', 'TransformationAlgorithmSequence'],
7014            '0x1515': ['CS', '1', 'OphthalmicAxialLengthMethod'],
7015            '0x1517': ['FL', '1', 'OphthalmicFOV'],
7016            '0x1518': ['SQ', '1', 'TwoDimensionalToThreeDimensionalMapSequence'],
7017            '0x1525': ['SQ', '1', 'WideFieldOphthalmicPhotographyQualityRatingSequence'],
7018            '0x1526': ['SQ', '1', 'WideFieldOphthalmicPhotographyQualityThresholdSequence'],
7019            '0x1527': ['FL', '1', 'WideFieldOphthalmicPhotographyThresholdQualityRating'],
7020            '0x1528': ['FL', '1', 'XCoordinatesCenterPixelViewAngle'],
7021            '0x1529': ['FL', '1', 'YCoordinatesCenterPixelViewAngle'],
7022            '0x1530': ['UL', '1', 'NumberOfMapPoints'],
7023            '0x1531': ['OF', '1', 'TwoDimensionalToThreeDimensionalMapData']
7024        },
7025        '0x0024': {
7026            '0x0000': ['UL', '1', 'GenericGroupLength'],
7027            '0x0010': ['FL', '1', 'VisualFieldHorizontalExtent'],
7028            '0x0011': ['FL', '1', 'VisualFieldVerticalExtent'],
7029            '0x0012': ['CS', '1', 'VisualFieldShape'],
7030            '0x0016': ['SQ', '1', 'ScreeningTestModeCodeSequence'],
7031            '0x0018': ['FL', '1', 'MaximumStimulusLuminance'],
7032            '0x0020': ['FL', '1', 'BackgroundLuminance'],
7033            '0x0021': ['SQ', '1', 'StimulusColorCodeSequence'],
7034            '0x0024': ['SQ', '1', 'BackgroundIlluminationColorCodeSequence'],
7035            '0x0025': ['FL', '1', 'StimulusArea'],
7036            '0x0028': ['FL', '1', 'StimulusPresentationTime'],
7037            '0x0032': ['SQ', '1', 'FixationSequence'],
7038            '0x0033': ['SQ', '1', 'FixationMonitoringCodeSequence'],
7039            '0x0034': ['SQ', '1', 'VisualFieldCatchTrialSequence'],
7040            '0x0035': ['US', '1', 'FixationCheckedQuantity'],
7041            '0x0036': ['US', '1', 'PatientNotProperlyFixatedQuantity'],
7042            '0x0037': ['CS', '1', 'PresentedVisualStimuliDataFlag'],
7043            '0x0038': ['US', '1', 'NumberOfVisualStimuli'],
7044            '0x0039': ['CS', '1', 'ExcessiveFixationLossesDataFlag'],
7045            '0x0040': ['CS', '1', 'ExcessiveFixationLosses'],
7046            '0x0042': ['US', '1', 'StimuliRetestingQuantity'],
7047            '0x0044': ['LT', '1', 'CommentsOnPatientPerformanceOfVisualField'],
7048            '0x0045': ['CS', '1', 'FalseNegativesEstimateFlag'],
7049            '0x0046': ['FL', '1', 'FalseNegativesEstimate'],
7050            '0x0048': ['US', '1', 'NegativeCatchTrialsQuantity'],
7051            '0x0050': ['US', '1', 'FalseNegativesQuantity'],
7052            '0x0051': ['CS', '1', 'ExcessiveFalseNegativesDataFlag'],
7053            '0x0052': ['CS', '1', 'ExcessiveFalseNegatives'],
7054            '0x0053': ['CS', '1', 'FalsePositivesEstimateFlag'],
7055            '0x0054': ['FL', '1', 'FalsePositivesEstimate'],
7056            '0x0055': ['CS', '1', 'CatchTrialsDataFlag'],
7057            '0x0056': ['US', '1', 'PositiveCatchTrialsQuantity'],
7058            '0x0057': ['CS', '1', 'TestPointNormalsDataFlag'],
7059            '0x0058': ['SQ', '1', 'TestPointNormalsSequence'],
7060            '0x0059': ['CS', '1', 'GlobalDeviationProbabilityNormalsFlag'],
7061            '0x0060': ['US', '1', 'FalsePositivesQuantity'],
7062            '0x0061': ['CS', '1', 'ExcessiveFalsePositivesDataFlag'],
7063            '0x0062': ['CS', '1', 'ExcessiveFalsePositives'],
7064            '0x0063': ['CS', '1', 'VisualFieldTestNormalsFlag'],
7065            '0x0064': ['SQ', '1', 'ResultsNormalsSequence'],
7066            '0x0065': ['SQ', '1', 'AgeCorrectedSensitivityDeviationAlgorithmSequence'],
7067            '0x0066': ['FL', '1', 'GlobalDeviationFromNormal'],
7068            '0x0067': ['SQ', '1', 'GeneralizedDefectSensitivityDeviationAlgorithmSequence'],
7069            '0x0068': ['FL', '1', 'LocalizedDeviationFromNormal'],
7070            '0x0069': ['LO', '1', 'PatientReliabilityIndicator'],
7071            '0x0070': ['FL', '1', 'VisualFieldMeanSensitivity'],
7072            '0x0071': ['FL', '1', 'GlobalDeviationProbability'],
7073            '0x0072': ['CS', '1', 'LocalDeviationProbabilityNormalsFlag'],
7074            '0x0073': ['FL', '1', 'LocalizedDeviationProbability'],
7075            '0x0074': ['CS', '1', 'ShortTermFluctuationCalculated'],
7076            '0x0075': ['FL', '1', 'ShortTermFluctuation'],
7077            '0x0076': ['CS', '1', 'ShortTermFluctuationProbabilityCalculated'],
7078            '0x0077': ['FL', '1', 'ShortTermFluctuationProbability'],
7079            '0x0078': ['CS', '1', 'CorrectedLocalizedDeviationFromNormalCalculated'],
7080            '0x0079': ['FL', '1', 'CorrectedLocalizedDeviationFromNormal'],
7081            '0x0080': ['CS', '1', 'CorrectedLocalizedDeviationFromNormalProbabilityCalculated'],
7082            '0x0081': ['FL', '1', 'CorrectedLocalizedDeviationFromNormalProbability'],
7083            '0x0083': ['SQ', '1', 'GlobalDeviationProbabilitySequence'],
7084            '0x0085': ['SQ', '1', 'LocalizedDeviationProbabilitySequence'],
7085            '0x0086': ['CS', '1', 'FovealSensitivityMeasured'],
7086            '0x0087': ['FL', '1', 'FovealSensitivity'],
7087            '0x0088': ['FL', '1', 'VisualFieldTestDuration'],
7088            '0x0089': ['SQ', '1', 'VisualFieldTestPointSequence'],
7089            '0x0090': ['FL', '1', 'VisualFieldTestPointXCoordinate'],
7090            '0x0091': ['FL', '1', 'VisualFieldTestPointYCoordinate'],
7091            '0x0092': ['FL', '1', 'AgeCorrectedSensitivityDeviationValue'],
7092            '0x0093': ['CS', '1', 'StimulusResults'],
7093            '0x0094': ['FL', '1', 'SensitivityValue'],
7094            '0x0095': ['CS', '1', 'RetestStimulusSeen'],
7095            '0x0096': ['FL', '1', 'RetestSensitivityValue'],
7096            '0x0097': ['SQ', '1', 'VisualFieldTestPointNormalsSequence'],
7097            '0x0098': ['FL', '1', 'QuantifiedDefect'],
7098            '0x0100': ['FL', '1', 'AgeCorrectedSensitivityDeviationProbabilityValue'],
7099            '0x0102': ['CS', '1', 'GeneralizedDefectCorrectedSensitivityDeviationFlag'],
7100            '0x0103': ['FL', '1', 'GeneralizedDefectCorrectedSensitivityDeviationValue'],
7101            '0x0104': ['FL', '1', 'GeneralizedDefectCorrectedSensitivityDeviationProbabilityValue'],
7102            '0x0105': ['FL', '1', 'MinimumSensitivityValue'],
7103            '0x0106': ['CS', '1', 'BlindSpotLocalized'],
7104            '0x0107': ['FL', '1', 'BlindSpotXCoordinate'],
7105            '0x0108': ['FL', '1', 'BlindSpotYCoordinate'],
7106            '0x0110': ['SQ', '1', 'VisualAcuityMeasurementSequence'],
7107            '0x0112': ['SQ', '1', 'RefractiveParametersUsedOnPatientSequence'],
7108            '0x0113': ['CS', '1', 'MeasurementLaterality'],
7109            '0x0114': ['SQ', '1', 'OphthalmicPatientClinicalInformationLeftEyeSequence'],
7110            '0x0115': ['SQ', '1', 'OphthalmicPatientClinicalInformationRightEyeSequence'],
7111            '0x0117': ['CS', '1', 'FovealPointNormativeDataFlag'],
7112            '0x0118': ['FL', '1', 'FovealPointProbabilityValue'],
7113            '0x0120': ['CS', '1', 'ScreeningBaselineMeasured'],
7114            '0x0122': ['SQ', '1', 'ScreeningBaselineMeasuredSequence'],
7115            '0x0124': ['CS', '1', 'ScreeningBaselineType'],
7116            '0x0126': ['FL', '1', 'ScreeningBaselineValue'],
7117            '0x0202': ['LO', '1', 'AlgorithmSource'],
7118            '0x0306': ['LO', '1', 'DataSetName'],
7119            '0x0307': ['LO', '1', 'DataSetVersion'],
7120            '0x0308': ['LO', '1', 'DataSetSource'],
7121            '0x0309': ['LO', '1', 'DataSetDescription'],
7122            '0x0317': ['SQ', '1', 'VisualFieldTestReliabilityGlobalIndexSequence'],
7123            '0x0320': ['SQ', '1', 'VisualFieldGlobalResultsIndexSequence'],
7124            '0x0325': ['SQ', '1', 'DataObservationSequence'],
7125            '0x0338': ['CS', '1', 'IndexNormalsFlag'],
7126            '0x0341': ['FL', '1', 'IndexProbability'],
7127            '0x0344': ['SQ', '1', 'IndexProbabilitySequence']
7128        },
7129        '0x0028': {
7130            '0x0000': ['UL', '1', 'GenericGroupLength'],
7131            '0x0002': ['US', '1', 'SamplesPerPixel'],
7132            '0x0003': ['US', '1', 'SamplesPerPixelUsed'],
7133            '0x0004': ['CS', '1', 'PhotometricInterpretation'],
7134            '0x0005': ['US', '1', 'ImageDimensions'],
7135            '0x0006': ['US', '1', 'PlanarConfiguration'],
7136            '0x0008': ['IS', '1', 'NumberOfFrames'],
7137            '0x0009': ['AT', '1-n', 'FrameIncrementPointer'],
7138            '0x000A': ['AT', '1-n', 'FrameDimensionPointer'],
7139            '0x0010': ['US', '1', 'Rows'],
7140            '0x0011': ['US', '1', 'Columns'],
7141            '0x0012': ['US', '1', 'Planes'],
7142            '0x0014': ['US', '1', 'UltrasoundColorDataPresent'],
7143            '0x0020': ['', '', ''],
7144            '0x0030': ['DS', '2', 'PixelSpacing'],
7145            '0x0031': ['DS', '2', 'ZoomFactor'],
7146            '0x0032': ['DS', '2', 'ZoomCenter'],
7147            '0x0034': ['IS', '2', 'PixelAspectRatio'],
7148            '0x0040': ['CS', '1', 'ImageFormat'],
7149            '0x0050': ['LO', '1-n', 'ManipulatedImage'],
7150            '0x0051': ['CS', '1-n', 'CorrectedImage'],
7151            '0x005F': ['LO', '1', 'CompressionRecognitionCode'],
7152            '0x0060': ['CS', '1', 'CompressionCode'],
7153            '0x0061': ['SH', '1', 'CompressionOriginator'],
7154            '0x0062': ['LO', '1', 'CompressionLabel'],
7155            '0x0063': ['SH', '1', 'CompressionDescription'],
7156            '0x0065': ['CS', '1-n', 'CompressionSequence'],
7157            '0x0066': ['AT', '1-n', 'CompressionStepPointers'],
7158            '0x0068': ['US', '1', 'RepeatInterval'],
7159            '0x0069': ['US', '1', 'BitsGrouped'],
7160            '0x0070': ['US', '1-n', 'PerimeterTable'],
7161            '0x0071': ['xs', '1', 'PerimeterValue'],
7162            '0x0080': ['US', '1', 'PredictorRows'],
7163            '0x0081': ['US', '1', 'PredictorColumns'],
7164            '0x0082': ['US', '1-n', 'PredictorConstants'],
7165            '0x0090': ['CS', '1', 'BlockedPixels'],
7166            '0x0091': ['US', '1', 'BlockRows'],
7167            '0x0092': ['US', '1', 'BlockColumns'],
7168            '0x0093': ['US', '1', 'RowOverlap'],
7169            '0x0094': ['US', '1', 'ColumnOverlap'],
7170            '0x0100': ['US', '1', 'BitsAllocated'],
7171            '0x0101': ['US', '1', 'BitsStored'],
7172            '0x0102': ['US', '1', 'HighBit'],
7173            '0x0103': ['US', '1', 'PixelRepresentation'],
7174            '0x0104': ['xs', '1', 'SmallestValidPixelValue'],
7175            '0x0105': ['xs', '1', 'LargestValidPixelValue'],
7176            '0x0106': ['xs', '1', 'SmallestImagePixelValue'],
7177            '0x0107': ['xs', '1', 'LargestImagePixelValue'],
7178            '0x0108': ['xs', '1', 'SmallestPixelValueInSeries'],
7179            '0x0109': ['xs', '1', 'LargestPixelValueInSeries'],
7180            '0x0110': ['xs', '1', 'SmallestImagePixelValueInPlane'],
7181            '0x0111': ['xs', '1', 'LargestImagePixelValueInPlane'],
7182            '0x0120': ['xs', '1', 'PixelPaddingValue'],
7183            '0x0121': ['xs', '1', 'PixelPaddingRangeLimit'],
7184            '0x0122': ['FL', '1', 'FloatPixelPaddingValue'],
7185            '0x0123': ['FD', '1', 'DoubleFloatPixelPaddingValue'],
7186            '0x0124': ['FL', '1', 'FloatPixelPaddingRangeLimit'],
7187            '0x0125': ['FD', '1', 'DoubleFloatPixelPaddingRangeLimit'],
7188            '0x0200': ['US', '1', 'ImageLocation'],
7189            '0x0300': ['CS', '1', 'QualityControlImage'],
7190            '0x0301': ['CS', '1', 'BurnedInAnnotation'],
7191            '0x0302': ['CS', '1', 'RecognizableVisualFeatures'],
7192            '0x0303': ['CS', '1', 'LongitudinalTemporalInformationModified'],
7193            '0x0304': ['UI', '1', 'ReferencedColorPaletteInstanceUID'],
7194            '0x0400': ['LO', '1', 'TransformLabel'],
7195            '0x0401': ['LO', '1', 'TransformVersionNumber'],
7196            '0x0402': ['US', '1', 'NumberOfTransformSteps'],
7197            '0x0403': ['LO', '1-n', 'SequenceOfCompressedData'],
7198            '0x0404': ['AT', '1-n', 'DetailsOfCoefficients'],
7199            '0x04x0': ['US', '1', 'RowsForNthOrderCoefficients'],
7200            '0x04x1': ['US', '1', 'ColumnsForNthOrderCoefficients'],
7201            '0x04x2': ['LO', '1-n', 'CoefficientCoding'],
7202            '0x04x3': ['AT', '1-n', 'CoefficientCodingPointers'],
7203            '0x0700': ['LO', '1', 'DCTLabel'],
7204            '0x0701': ['CS', '1-n', 'DataBlockDescription'],
7205            '0x0702': ['AT', '1-n', 'DataBlock'],
7206            '0x0710': ['US', '1', 'NormalizationFactorFormat'],
7207            '0x0720': ['US', '1', 'ZonalMapNumberFormat'],
7208            '0x0721': ['AT', '1-n', 'ZonalMapLocation'],
7209            '0x0722': ['US', '1', 'ZonalMapFormat'],
7210            '0x0730': ['US', '1', 'AdaptiveMapFormat'],
7211            '0x0740': ['US', '1', 'CodeNumberFormat'],
7212            '0x08x0': ['CS', '1-n', 'CodeLabel'],
7213            '0x08x2': ['US', '1', 'NumberOfTables'],
7214            '0x08x3': ['AT', '1-n', 'CodeTableLocation'],
7215            '0x08x4': ['US', '1', 'BitsForCodeWord'],
7216            '0x08x8': ['AT', '1-n', 'ImageDataLocation'],
7217            '0x0A02': ['CS', '1', 'PixelSpacingCalibrationType'],
7218            '0x0A04': ['LO', '1', 'PixelSpacingCalibrationDescription'],
7219            '0x1040': ['CS', '1', 'PixelIntensityRelationship'],
7220            '0x1041': ['SS', '1', 'PixelIntensityRelationshipSign'],
7221            '0x1050': ['DS', '1-n', 'WindowCenter'],
7222            '0x1051': ['DS', '1-n', 'WindowWidth'],
7223            '0x1052': ['DS', '1', 'RescaleIntercept'],
7224            '0x1053': ['DS', '1', 'RescaleSlope'],
7225            '0x1054': ['LO', '1', 'RescaleType'],
7226            '0x1055': ['LO', '1-n', 'WindowCenterWidthExplanation'],
7227            '0x1056': ['CS', '1', 'VOILUTFunction'],
7228            '0x1080': ['CS', '1', 'GrayScale'],
7229            '0x1090': ['CS', '1', 'RecommendedViewingMode'],
7230            '0x1100': ['xs', '3', 'GrayLookupTableDescriptor'],
7231            '0x1101': ['xs', '3', 'RedPaletteColorLookupTableDescriptor'],
7232            '0x1102': ['xs', '3', 'GreenPaletteColorLookupTableDescriptor'],
7233            '0x1103': ['xs', '3', 'BluePaletteColorLookupTableDescriptor'],
7234            '0x1104': ['US', '3', 'AlphaPaletteColorLookupTableDescriptor'],
7235            '0x1111': ['xs', '4', 'LargeRedPaletteColorLookupTableDescriptor'],
7236            '0x1112': ['xs', '4', 'LargeGreenPaletteColorLookupTableDescriptor'],
7237            '0x1113': ['xs', '4', 'LargeBluePaletteColorLookupTableDescriptor'],
7238            '0x1199': ['UI', '1', 'PaletteColorLookupTableUID'],
7239            '0x1200': ['US or SS or OW', '1-n or 1', 'GrayLookupTableData'],
7240            '0x1201': ['OW', '1', 'RedPaletteColorLookupTableData'],
7241            '0x1202': ['OW', '1', 'GreenPaletteColorLookupTableData'],
7242            '0x1203': ['OW', '1', 'BluePaletteColorLookupTableData'],
7243            '0x1204': ['OW', '1', 'AlphaPaletteColorLookupTableData'],
7244            '0x1211': ['OW', '1', 'LargeRedPaletteColorLookupTableData'],
7245            '0x1212': ['OW', '1', 'LargeGreenPaletteColorLookupTableData'],
7246            '0x1213': ['OW', '1', 'LargeBluePaletteColorLookupTableData'],
7247            '0x1214': ['UI', '1', 'LargePaletteColorLookupTableUID'],
7248            '0x1221': ['OW', '1', 'SegmentedRedPaletteColorLookupTableData'],
7249            '0x1222': ['OW', '1', 'SegmentedGreenPaletteColorLookupTableData'],
7250            '0x1223': ['OW', '1', 'SegmentedBluePaletteColorLookupTableData'],
7251            '0x1300': ['CS', '1', 'BreastImplantPresent'],
7252            '0x1350': ['CS', '1', 'PartialView'],
7253            '0x1351': ['ST', '1', 'PartialViewDescription'],
7254            '0x1352': ['SQ', '1', 'PartialViewCodeSequence'],
7255            '0x135A': ['CS', '1', 'SpatialLocationsPreserved'],
7256            '0x1401': ['SQ', '1', 'DataFrameAssignmentSequence'],
7257            '0x1402': ['CS', '1', 'DataPathAssignment'],
7258            '0x1403': ['US', '1', 'BitsMappedToColorLookupTable'],
7259            '0x1404': ['SQ', '1', 'BlendingLUT1Sequence'],
7260            '0x1405': ['CS', '1', 'BlendingLUT1TransferFunction'],
7261            '0x1406': ['FD', '1', 'BlendingWeightConstant'],
7262            '0x1407': ['US', '3', 'BlendingLookupTableDescriptor'],
7263            '0x1408': ['OW', '1', 'BlendingLookupTableData'],
7264            '0x140B': ['SQ', '1', 'EnhancedPaletteColorLookupTableSequence'],
7265            '0x140C': ['SQ', '1', 'BlendingLUT2Sequence'],
7266            '0x140D': ['CS', '1', 'BlendingLUT2TransferFunction'],
7267            '0x140E': ['CS', '1', 'DataPathID'],
7268            '0x140F': ['CS', '1', 'RGBLUTTransferFunction'],
7269            '0x1410': ['CS', '1', 'AlphaLUTTransferFunction'],
7270            '0x2000': ['OB', '1', 'ICCProfile'],
7271            '0x2110': ['CS', '1', 'LossyImageCompression'],
7272            '0x2112': ['DS', '1-n', 'LossyImageCompressionRatio'],
7273            '0x2114': ['CS', '1-n', 'LossyImageCompressionMethod'],
7274            '0x3000': ['SQ', '1', 'ModalityLUTSequence'],
7275            '0x3002': ['xs', '3', 'LUTDescriptor'],
7276            '0x3003': ['LO', '1', 'LUTExplanation'],
7277            '0x3004': ['LO', '1', 'ModalityLUTType'],
7278            '0x3006': ['US or OW', '1-n or 1', 'LUTData'],
7279            '0x3010': ['SQ', '1', 'VOILUTSequence'],
7280            '0x3110': ['SQ', '1', 'SoftcopyVOILUTSequence'],
7281            '0x4000': ['LT', '1', 'ImagePresentationComments'],
7282            '0x5000': ['SQ', '1', 'BiPlaneAcquisitionSequence'],
7283            '0x6010': ['US', '1', 'RepresentativeFrameNumber'],
7284            '0x6020': ['US', '1-n', 'FrameNumbersOfInterest'],
7285            '0x6022': ['LO', '1-n', 'FrameOfInterestDescription'],
7286            '0x6023': ['CS', '1-n', 'FrameOfInterestType'],
7287            '0x6030': ['US', '1-n', 'MaskPointers'],
7288            '0x6040': ['US', '1-n', 'RWavePointer'],
7289            '0x6100': ['SQ', '1', 'MaskSubtractionSequence'],
7290            '0x6101': ['CS', '1', 'MaskOperation'],
7291            '0x6102': ['US', '2-2n', 'ApplicableFrameRange'],
7292            '0x6110': ['US', '1-n', 'MaskFrameNumbers'],
7293            '0x6112': ['US', '1', 'ContrastFrameAveraging'],
7294            '0x6114': ['FL', '2', 'MaskSubPixelShift'],
7295            '0x6120': ['SS', '1', 'TIDOffset'],
7296            '0x6190': ['ST', '1', 'MaskOperationExplanation'],
7297            '0x7000': ['SQ', '1', 'EquipmentAdministratorSequence'],
7298            '0x7001': ['US', '1', 'NumberOfDisplaySubsystems'],
7299            '0x7002': ['US', '1', 'CurrentConfigurationID'],
7300            '0x7003': ['US', '1', 'DisplaySubsystemID'],
7301            '0x7004': ['SH', '1', 'DisplaySubsystemName'],
7302            '0x7005': ['LO', '1', 'DisplaySubsystemDescription'],
7303            '0x7006': ['CS', '1', 'SystemStatus'],
7304            '0x7007': ['LO', '1', 'SystemStatusComment'],
7305            '0x7008': ['SQ', '1', 'TargetLuminanceCharacteristicsSequence'],
7306            '0x7009': ['US', '1', 'LuminanceCharacteristicsID'],
7307            '0x700A': ['SQ', '1', 'DisplaySubsystemConfigurationSequence'],
7308            '0x700B': ['US', '1', 'ConfigurationID'],
7309            '0x700C': ['SH', '1', 'ConfigurationName'],
7310            '0x700D': ['LO', '1', 'ConfigurationDescription'],
7311            '0x700E': ['US', '1', 'ReferencedTargetLuminanceCharacteristicsID'],
7312            '0x700F': ['SQ', '1', 'QAResultsSequence'],
7313            '0x7010': ['SQ', '1', 'DisplaySubsystemQAResultsSequence'],
7314            '0x7011': ['SQ', '1', 'ConfigurationQAResultsSequence'],
7315            '0x7012': ['SQ', '1', 'MeasurementEquipmentSequence'],
7316            '0x7013': ['CS', '1-n', 'MeasurementFunctions'],
7317            '0x7014': ['CS', '1', 'MeasurementEquipmentType'],
7318            '0x7015': ['SQ', '1', 'VisualEvaluationResultSequence'],
7319            '0x7016': ['SQ', '1', 'DisplayCalibrationResultSequence'],
7320            '0x7017': ['US', '1', 'DDLValue'],
7321            '0x7018': ['FL', '2', 'CIExyWhitePoint'],
7322            '0x7019': ['CS', '1', 'DisplayFunctionType'],
7323            '0x701A': ['FL', '1', 'GammaValue'],
7324            '0x701B': ['US', '1', 'NumberOfLuminancePoints'],
7325            '0x701C': ['SQ', '1', 'LuminanceResponseSequence'],
7326            '0x701D': ['FL', '1', 'TargetMinimumLuminance'],
7327            '0x701E': ['FL', '1', 'TargetMaximumLuminance'],
7328            '0x701F': ['FL', '1', 'LuminanceValue'],
7329            '0x7020': ['LO', '1', 'LuminanceResponseDescription'],
7330            '0x7021': ['CS', '1', 'WhitePointFlag'],
7331            '0x7022': ['SQ', '1', 'DisplayDeviceTypeCodeSequence'],
7332            '0x7023': ['SQ', '1', 'DisplaySubsystemSequence'],
7333            '0x7024': ['SQ', '1', 'LuminanceResultSequence'],
7334            '0x7025': ['CS', '1', 'AmbientLightValueSource'],
7335            '0x7026': ['CS', '1-n', 'MeasuredCharacteristics'],
7336            '0x7027': ['SQ', '1', 'LuminanceUniformityResultSequence'],
7337            '0x7028': ['SQ', '1', 'VisualEvaluationTestSequence'],
7338            '0x7029': ['CS', '1', 'TestResult'],
7339            '0x702A': ['LO', '1', 'TestResultComment'],
7340            '0x702B': ['CS', '1', 'TestImageValidation'],
7341            '0x702C': ['SQ', '1', 'TestPatternCodeSequence'],
7342            '0x702D': ['SQ', '1', 'MeasurementPatternCodeSequence'],
7343            '0x702E': ['SQ', '1', 'VisualEvaluationMethodCodeSequence'],
7344            '0x7FE0': ['UR', '1', 'PixelDataProviderURL'],
7345            '0x9001': ['UL', '1', 'DataPointRows'],
7346            '0x9002': ['UL', '1', 'DataPointColumns'],
7347            '0x9003': ['CS', '1', 'SignalDomainColumns'],
7348            '0x9099': ['US', '1', 'LargestMonochromePixelValue'],
7349            '0x9108': ['CS', '1', 'DataRepresentation'],
7350            '0x9110': ['SQ', '1', 'PixelMeasuresSequence'],
7351            '0x9132': ['SQ', '1', 'FrameVOILUTSequence'],
7352            '0x9145': ['SQ', '1', 'PixelValueTransformationSequence'],
7353            '0x9235': ['CS', '1', 'SignalDomainRows'],
7354            '0x9411': ['FL', '1', 'DisplayFilterPercentage'],
7355            '0x9415': ['SQ', '1', 'FramePixelShiftSequence'],
7356            '0x9416': ['US', '1', 'SubtractionItemID'],
7357            '0x9422': ['SQ', '1', 'PixelIntensityRelationshipLUTSequence'],
7358            '0x9443': ['SQ', '1', 'FramePixelDataPropertiesSequence'],
7359            '0x9444': ['CS', '1', 'GeometricalProperties'],
7360            '0x9445': ['FL', '1', 'GeometricMaximumDistortion'],
7361            '0x9446': ['CS', '1-n', 'ImageProcessingApplied'],
7362            '0x9454': ['CS', '1', 'MaskSelectionMode'],
7363            '0x9474': ['CS', '1', 'LUTFunction'],
7364            '0x9478': ['FL', '1', 'MaskVisibilityPercentage'],
7365            '0x9501': ['SQ', '1', 'PixelShiftSequence'],
7366            '0x9502': ['SQ', '1', 'RegionPixelShiftSequence'],
7367            '0x9503': ['SS', '2-2n', 'VerticesOfTheRegion'],
7368            '0x9505': ['SQ', '1', 'MultiFramePresentationSequence'],
7369            '0x9506': ['US', '2-2n', 'PixelShiftFrameRange'],
7370            '0x9507': ['US', '2-2n', 'LUTFrameRange'],
7371            '0x9520': ['DS', '16', 'ImageToEquipmentMappingMatrix'],
7372            '0x9537': ['CS', '1', 'EquipmentCoordinateSystemIdentification']
7373        },
7374        '0x0032': {
7375            '0x0000': ['UL', '1', 'GenericGroupLength'],
7376            '0x000A': ['CS', '1', 'StudyStatusID'],
7377            '0x000C': ['CS', '1', 'StudyPriorityID'],
7378            '0x0012': ['LO', '1', 'StudyIDIssuer'],
7379            '0x0032': ['DA', '1', 'StudyVerifiedDate'],
7380            '0x0033': ['TM', '1', 'StudyVerifiedTime'],
7381            '0x0034': ['DA', '1', 'StudyReadDate'],
7382            '0x0035': ['TM', '1', 'StudyReadTime'],
7383            '0x1000': ['DA', '1', 'ScheduledStudyStartDate'],
7384            '0x1001': ['TM', '1', 'ScheduledStudyStartTime'],
7385            '0x1010': ['DA', '1', 'ScheduledStudyStopDate'],
7386            '0x1011': ['TM', '1', 'ScheduledStudyStopTime'],
7387            '0x1020': ['LO', '1', 'ScheduledStudyLocation'],
7388            '0x1021': ['AE', '1-n', 'ScheduledStudyLocationAETitle'],
7389            '0x1030': ['LO', '1', 'ReasonForStudy'],
7390            '0x1031': ['SQ', '1', 'RequestingPhysicianIdentificationSequence'],
7391            '0x1032': ['PN', '1', 'RequestingPhysician'],
7392            '0x1033': ['LO', '1', 'RequestingService'],
7393            '0x1034': ['SQ', '1', 'RequestingServiceCodeSequence'],
7394            '0x1040': ['DA', '1', 'StudyArrivalDate'],
7395            '0x1041': ['TM', '1', 'StudyArrivalTime'],
7396            '0x1050': ['DA', '1', 'StudyCompletionDate'],
7397            '0x1051': ['TM', '1', 'StudyCompletionTime'],
7398            '0x1055': ['CS', '1', 'StudyComponentStatusID'],
7399            '0x1060': ['LO', '1', 'RequestedProcedureDescription'],
7400            '0x1064': ['SQ', '1', 'RequestedProcedureCodeSequence'],
7401            '0x1070': ['LO', '1', 'RequestedContrastAgent'],
7402            '0x4000': ['LT', '1', 'StudyComments']
7403        },
7404        '0x0038': {
7405            '0x0000': ['UL', '1', 'GenericGroupLength'],
7406            '0x0004': ['SQ', '1', 'ReferencedPatientAliasSequence'],
7407            '0x0008': ['CS', '1', 'VisitStatusID'],
7408            '0x0010': ['LO', '1', 'AdmissionID'],
7409            '0x0011': ['LO', '1', 'IssuerOfAdmissionID'],
7410            '0x0014': ['SQ', '1', 'IssuerOfAdmissionIDSequence'],
7411            '0x0016': ['LO', '1', 'RouteOfAdmissions'],
7412            '0x001A': ['DA', '1', 'ScheduledAdmissionDate'],
7413            '0x001B': ['TM', '1', 'ScheduledAdmissionTime'],
7414            '0x001C': ['DA', '1', 'ScheduledDischargeDate'],
7415            '0x001D': ['TM', '1', 'ScheduledDischargeTime'],
7416            '0x001E': ['LO', '1', 'ScheduledPatientInstitutionResidence'],
7417            '0x0020': ['DA', '1', 'AdmittingDate'],
7418            '0x0021': ['TM', '1', 'AdmittingTime'],
7419            '0x0030': ['DA', '1', 'DischargeDate'],
7420            '0x0032': ['TM', '1', 'DischargeTime'],
7421            '0x0040': ['LO', '1', 'DischargeDiagnosisDescription'],
7422            '0x0044': ['SQ', '1', 'DischargeDiagnosisCodeSequence'],
7423            '0x0050': ['LO', '1', 'SpecialNeeds'],
7424            '0x0060': ['LO', '1', 'ServiceEpisodeID'],
7425            '0x0061': ['LO', '1', 'IssuerOfServiceEpisodeID'],
7426            '0x0062': ['LO', '1', 'ServiceEpisodeDescription'],
7427            '0x0064': ['SQ', '1', 'IssuerOfServiceEpisodeIDSequence'],
7428            '0x0100': ['SQ', '1', 'PertinentDocumentsSequence'],
7429            '0x0101': ['SQ', '1', 'PertinentResourcesSequence'],
7430            '0x0102': ['LO', '1', 'ResourceDescription'],
7431            '0x0300': ['LO', '1', 'CurrentPatientLocation'],
7432            '0x0400': ['LO', '1', 'PatientInstitutionResidence'],
7433            '0x0500': ['LO', '1', 'PatientState'],
7434            '0x0502': ['SQ', '1', 'PatientClinicalTrialParticipationSequence'],
7435            '0x4000': ['LT', '1', 'VisitComments']
7436        },
7437        '0x003A': {
7438            '0x0000': ['UL', '1', 'GenericGroupLength'],
7439            '0x0004': ['CS', '1', 'WaveformOriginality'],
7440            '0x0005': ['US', '1', 'NumberOfWaveformChannels'],
7441            '0x0010': ['UL', '1', 'NumberOfWaveformSamples'],
7442            '0x001A': ['DS', '1', 'SamplingFrequency'],
7443            '0x0020': ['SH', '1', 'MultiplexGroupLabel'],
7444            '0x0200': ['SQ', '1', 'ChannelDefinitionSequence'],
7445            '0x0202': ['IS', '1', 'WaveformChannelNumber'],
7446            '0x0203': ['SH', '1', 'ChannelLabel'],
7447            '0x0205': ['CS', '1-n', 'ChannelStatus'],
7448            '0x0208': ['SQ', '1', 'ChannelSourceSequence'],
7449            '0x0209': ['SQ', '1', 'ChannelSourceModifiersSequence'],
7450            '0x020A': ['SQ', '1', 'SourceWaveformSequence'],
7451            '0x020C': ['LO', '1', 'ChannelDerivationDescription'],
7452            '0x0210': ['DS', '1', 'ChannelSensitivity'],
7453            '0x0211': ['SQ', '1', 'ChannelSensitivityUnitsSequence'],
7454            '0x0212': ['DS', '1', 'ChannelSensitivityCorrectionFactor'],
7455            '0x0213': ['DS', '1', 'ChannelBaseline'],
7456            '0x0214': ['DS', '1', 'ChannelTimeSkew'],
7457            '0x0215': ['DS', '1', 'ChannelSampleSkew'],
7458            '0x0218': ['DS', '1', 'ChannelOffset'],
7459            '0x021A': ['US', '1', 'WaveformBitsStored'],
7460            '0x0220': ['DS', '1', 'FilterLowFrequency'],
7461            '0x0221': ['DS', '1', 'FilterHighFrequency'],
7462            '0x0222': ['DS', '1', 'NotchFilterFrequency'],
7463            '0x0223': ['DS', '1', 'NotchFilterBandwidth'],
7464            '0x0230': ['FL', '1', 'WaveformDataDisplayScale'],
7465            '0x0231': ['US', '3', 'WaveformDisplayBackgroundCIELabValue'],
7466            '0x0240': ['SQ', '1', 'WaveformPresentationGroupSequence'],
7467            '0x0241': ['US', '1', 'PresentationGroupNumber'],
7468            '0x0242': ['SQ', '1', 'ChannelDisplaySequence'],
7469            '0x0244': ['US', '3', 'ChannelRecommendedDisplayCIELabValue'],
7470            '0x0245': ['FL', '1', 'ChannelPosition'],
7471            '0x0246': ['CS', '1', 'DisplayShadingFlag'],
7472            '0x0247': ['FL', '1', 'FractionalChannelDisplayScale'],
7473            '0x0248': ['FL', '1', 'AbsoluteChannelDisplayScale'],
7474            '0x0300': ['SQ', '1', 'MultiplexedAudioChannelsDescriptionCodeSequence'],
7475            '0x0301': ['IS', '1', 'ChannelIdentificationCode'],
7476            '0x0302': ['CS', '1', 'ChannelMode']
7477        },
7478        '0x0040': {
7479            '0x0000': ['UL', '1', 'GenericGroupLength'],
7480            '0x0001': ['AE', '1-n', 'ScheduledStationAETitle'],
7481            '0x0002': ['DA', '1', 'ScheduledProcedureStepStartDate'],
7482            '0x0003': ['TM', '1', 'ScheduledProcedureStepStartTime'],
7483            '0x0004': ['DA', '1', 'ScheduledProcedureStepEndDate'],
7484            '0x0005': ['TM', '1', 'ScheduledProcedureStepEndTime'],
7485            '0x0006': ['PN', '1', 'ScheduledPerformingPhysicianName'],
7486            '0x0007': ['LO', '1', 'ScheduledProcedureStepDescription'],
7487            '0x0008': ['SQ', '1', 'ScheduledProtocolCodeSequence'],
7488            '0x0009': ['SH', '1', 'ScheduledProcedureStepID'],
7489            '0x000A': ['SQ', '1', 'StageCodeSequence'],
7490            '0x000B': ['SQ', '1', 'ScheduledPerformingPhysicianIdentificationSequence'],
7491            '0x0010': ['SH', '1-n', 'ScheduledStationName'],
7492            '0x0011': ['SH', '1', 'ScheduledProcedureStepLocation'],
7493            '0x0012': ['LO', '1', 'PreMedication'],
7494            '0x0020': ['CS', '1', 'ScheduledProcedureStepStatus'],
7495            '0x0026': ['SQ', '1', 'OrderPlacerIdentifierSequence'],
7496            '0x0027': ['SQ', '1', 'OrderFillerIdentifierSequence'],
7497            '0x0031': ['UT', '1', 'LocalNamespaceEntityID'],
7498            '0x0032': ['UT', '1', 'UniversalEntityID'],
7499            '0x0033': ['CS', '1', 'UniversalEntityIDType'],
7500            '0x0035': ['CS', '1', 'IdentifierTypeCode'],
7501            '0x0036': ['SQ', '1', 'AssigningFacilitySequence'],
7502            '0x0039': ['SQ', '1', 'AssigningJurisdictionCodeSequence'],
7503            '0x003A': ['SQ', '1', 'AssigningAgencyOrDepartmentCodeSequence'],
7504            '0x0100': ['SQ', '1', 'ScheduledProcedureStepSequence'],
7505            '0x0220': ['SQ', '1', 'ReferencedNonImageCompositeSOPInstanceSequence'],
7506            '0x0241': ['AE', '1', 'PerformedStationAETitle'],
7507            '0x0242': ['SH', '1', 'PerformedStationName'],
7508            '0x0243': ['SH', '1', 'PerformedLocation'],
7509            '0x0244': ['DA', '1', 'PerformedProcedureStepStartDate'],
7510            '0x0245': ['TM', '1', 'PerformedProcedureStepStartTime'],
7511            '0x0250': ['DA', '1', 'PerformedProcedureStepEndDate'],
7512            '0x0251': ['TM', '1', 'PerformedProcedureStepEndTime'],
7513            '0x0252': ['CS', '1', 'PerformedProcedureStepStatus'],
7514            '0x0253': ['SH', '1', 'PerformedProcedureStepID'],
7515            '0x0254': ['LO', '1', 'PerformedProcedureStepDescription'],
7516            '0x0255': ['LO', '1', 'PerformedProcedureTypeDescription'],
7517            '0x0260': ['SQ', '1', 'PerformedProtocolCodeSequence'],
7518            '0x0261': ['CS', '1', 'PerformedProtocolType'],
7519            '0x0270': ['SQ', '1', 'ScheduledStepAttributesSequence'],
7520            '0x0275': ['SQ', '1', 'RequestAttributesSequence'],
7521            '0x0280': ['ST', '1', 'CommentsOnThePerformedProcedureStep'],
7522            '0x0281': ['SQ', '1', 'PerformedProcedureStepDiscontinuationReasonCodeSequence'],
7523            '0x0293': ['SQ', '1', 'QuantitySequence'],
7524            '0x0294': ['DS', '1', 'Quantity'],
7525            '0x0295': ['SQ', '1', 'MeasuringUnitsSequence'],
7526            '0x0296': ['SQ', '1', 'BillingItemSequence'],
7527            '0x0300': ['US', '1', 'TotalTimeOfFluoroscopy'],
7528            '0x0301': ['US', '1', 'TotalNumberOfExposures'],
7529            '0x0302': ['US', '1', 'EntranceDose'],
7530            '0x0303': ['US', '1-2', 'ExposedArea'],
7531            '0x0306': ['DS', '1', 'DistanceSourceToEntrance'],
7532            '0x0307': ['DS', '1', 'DistanceSourceToSupport'],
7533            '0x030E': ['SQ', '1', 'ExposureDoseSequence'],
7534            '0x0310': ['ST', '1', 'CommentsOnRadiationDose'],
7535            '0x0312': ['DS', '1', 'XRayOutput'],
7536            '0x0314': ['DS', '1', 'HalfValueLayer'],
7537            '0x0316': ['DS', '1', 'OrganDose'],
7538            '0x0318': ['CS', '1', 'OrganExposed'],
7539            '0x0320': ['SQ', '1', 'BillingProcedureStepSequence'],
7540            '0x0321': ['SQ', '1', 'FilmConsumptionSequence'],
7541            '0x0324': ['SQ', '1', 'BillingSuppliesAndDevicesSequence'],
7542            '0x0330': ['SQ', '1', 'ReferencedProcedureStepSequence'],
7543            '0x0340': ['SQ', '1', 'PerformedSeriesSequence'],
7544            '0x0400': ['LT', '1', 'CommentsOnTheScheduledProcedureStep'],
7545            '0x0440': ['SQ', '1', 'ProtocolContextSequence'],
7546            '0x0441': ['SQ', '1', 'ContentItemModifierSequence'],
7547            '0x0500': ['SQ', '1', 'ScheduledSpecimenSequence'],
7548            '0x050A': ['LO', '1', 'SpecimenAccessionNumber'],
7549            '0x0512': ['LO', '1', 'ContainerIdentifier'],
7550            '0x0513': ['SQ', '1', 'IssuerOfTheContainerIdentifierSequence'],
7551            '0x0515': ['SQ', '1', 'AlternateContainerIdentifierSequence'],
7552            '0x0518': ['SQ', '1', 'ContainerTypeCodeSequence'],
7553            '0x051A': ['LO', '1', 'ContainerDescription'],
7554            '0x0520': ['SQ', '1', 'ContainerComponentSequence'],
7555            '0x0550': ['SQ', '1', 'SpecimenSequence'],
7556            '0x0551': ['LO', '1', 'SpecimenIdentifier'],
7557            '0x0552': ['SQ', '1', 'SpecimenDescriptionSequenceTrial'],
7558            '0x0553': ['ST', '1', 'SpecimenDescriptionTrial'],
7559            '0x0554': ['UI', '1', 'SpecimenUID'],
7560            '0x0555': ['SQ', '1', 'AcquisitionContextSequence'],
7561            '0x0556': ['ST', '1', 'AcquisitionContextDescription'],
7562            '0x059A': ['SQ', '1', 'SpecimenTypeCodeSequence'],
7563            '0x0560': ['SQ', '1', 'SpecimenDescriptionSequence'],
7564            '0x0562': ['SQ', '1', 'IssuerOfTheSpecimenIdentifierSequence'],
7565            '0x0600': ['LO', '1', 'SpecimenShortDescription'],
7566            '0x0602': ['UT', '1', 'SpecimenDetailedDescription'],
7567            '0x0610': ['SQ', '1', 'SpecimenPreparationSequence'],
7568            '0x0612': ['SQ', '1', 'SpecimenPreparationStepContentItemSequence'],
7569            '0x0620': ['SQ', '1', 'SpecimenLocalizationContentItemSequence'],
7570            '0x06FA': ['LO', '1', 'SlideIdentifier'],
7571            '0x071A': ['SQ', '1', 'ImageCenterPointCoordinatesSequence'],
7572            '0x072A': ['DS', '1', 'XOffsetInSlideCoordinateSystem'],
7573            '0x073A': ['DS', '1', 'YOffsetInSlideCoordinateSystem'],
7574            '0x074A': ['DS', '1', 'ZOffsetInSlideCoordinateSystem'],
7575            '0x08D8': ['SQ', '1', 'PixelSpacingSequence'],
7576            '0x08DA': ['SQ', '1', 'CoordinateSystemAxisCodeSequence'],
7577            '0x08EA': ['SQ', '1', 'MeasurementUnitsCodeSequence'],
7578            '0x09F8': ['SQ', '1', 'VitalStainCodeSequenceTrial'],
7579            '0x1001': ['SH', '1', 'RequestedProcedureID'],
7580            '0x1002': ['LO', '1', 'ReasonForTheRequestedProcedure'],
7581            '0x1003': ['SH', '1', 'RequestedProcedurePriority'],
7582            '0x1004': ['LO', '1', 'PatientTransportArrangements'],
7583            '0x1005': ['LO', '1', 'RequestedProcedureLocation'],
7584            '0x1006': ['SH', '1', 'PlacerOrderNumberProcedure'],
7585            '0x1007': ['SH', '1', 'FillerOrderNumberProcedure'],
7586            '0x1008': ['LO', '1', 'ConfidentialityCode'],
7587            '0x1009': ['SH', '1', 'ReportingPriority'],
7588            '0x100A': ['SQ', '1', 'ReasonForRequestedProcedureCodeSequence'],
7589            '0x1010': ['PN', '1-n', 'NamesOfIntendedRecipientsOfResults'],
7590            '0x1011': ['SQ', '1', 'IntendedRecipientsOfResultsIdentificationSequence'],
7591            '0x1012': ['SQ', '1', 'ReasonForPerformedProcedureCodeSequence'],
7592            '0x1060': ['LO', '1', 'RequestedProcedureDescriptionTrial'],
7593            '0x1101': ['SQ', '1', 'PersonIdentificationCodeSequence'],
7594            '0x1102': ['ST', '1', 'PersonAddress'],
7595            '0x1103': ['LO', '1-n', 'PersonTelephoneNumbers'],
7596            '0x1104': ['LT', '1', 'PersonTelecomInformation'],
7597            '0x1400': ['LT', '1', 'RequestedProcedureComments'],
7598            '0x2001': ['LO', '1', 'ReasonForTheImagingServiceRequest'],
7599            '0x2004': ['DA', '1', 'IssueDateOfImagingServiceRequest'],
7600            '0x2005': ['TM', '1', 'IssueTimeOfImagingServiceRequest'],
7601            '0x2006': ['SH', '1', 'PlacerOrderNumberImagingServiceRequestRetired'],
7602            '0x2007': ['SH', '1', 'FillerOrderNumberImagingServiceRequestRetired'],
7603            '0x2008': ['PN', '1', 'OrderEnteredBy'],
7604            '0x2009': ['SH', '1', 'OrderEntererLocation'],
7605            '0x2010': ['SH', '1', 'OrderCallbackPhoneNumber'],
7606            '0x2011': ['LT', '1', 'OrderCallbackTelecomInformation'],
7607            '0x2016': ['LO', '1', 'PlacerOrderNumberImagingServiceRequest'],
7608            '0x2017': ['LO', '1', 'FillerOrderNumberImagingServiceRequest'],
7609            '0x2400': ['LT', '1', 'ImagingServiceRequestComments'],
7610            '0x3001': ['LO', '1', 'ConfidentialityConstraintOnPatientDataDescription'],
7611            '0x4001': ['CS', '1', 'GeneralPurposeScheduledProcedureStepStatus'],
7612            '0x4002': ['CS', '1', 'GeneralPurposePerformedProcedureStepStatus'],
7613            '0x4003': ['CS', '1', 'GeneralPurposeScheduledProcedureStepPriority'],
7614            '0x4004': ['SQ', '1', 'ScheduledProcessingApplicationsCodeSequence'],
7615            '0x4005': ['DT', '1', 'ScheduledProcedureStepStartDateTime'],
7616            '0x4006': ['CS', '1', 'MultipleCopiesFlag'],
7617            '0x4007': ['SQ', '1', 'PerformedProcessingApplicationsCodeSequence'],
7618            '0x4009': ['SQ', '1', 'HumanPerformerCodeSequence'],
7619            '0x4010': ['DT', '1', 'ScheduledProcedureStepModificationDateTime'],
7620            '0x4011': ['DT', '1', 'ExpectedCompletionDateTime'],
7621            '0x4015': ['SQ', '1', 'ResultingGeneralPurposePerformedProcedureStepsSequence'],
7622            '0x4016': ['SQ', '1', 'ReferencedGeneralPurposeScheduledProcedureStepSequence'],
7623            '0x4018': ['SQ', '1', 'ScheduledWorkitemCodeSequence'],
7624            '0x4019': ['SQ', '1', 'PerformedWorkitemCodeSequence'],
7625            '0x4020': ['CS', '1', 'InputAvailabilityFlag'],
7626            '0x4021': ['SQ', '1', 'InputInformationSequence'],
7627            '0x4022': ['SQ', '1', 'RelevantInformationSequence'],
7628            '0x4023': ['UI', '1', 'ReferencedGeneralPurposeScheduledProcedureStepTransactionUID'],
7629            '0x4025': ['SQ', '1', 'ScheduledStationNameCodeSequence'],
7630            '0x4026': ['SQ', '1', 'ScheduledStationClassCodeSequence'],
7631            '0x4027': ['SQ', '1', 'ScheduledStationGeographicLocationCodeSequence'],
7632            '0x4028': ['SQ', '1', 'PerformedStationNameCodeSequence'],
7633            '0x4029': ['SQ', '1', 'PerformedStationClassCodeSequence'],
7634            '0x4030': ['SQ', '1', 'PerformedStationGeographicLocationCodeSequence'],
7635            '0x4031': ['SQ', '1', 'RequestedSubsequentWorkitemCodeSequence'],
7636            '0x4032': ['SQ', '1', 'NonDICOMOutputCodeSequence'],
7637            '0x4033': ['SQ', '1', 'OutputInformationSequence'],
7638            '0x4034': ['SQ', '1', 'ScheduledHumanPerformersSequence'],
7639            '0x4035': ['SQ', '1', 'ActualHumanPerformersSequence'],
7640            '0x4036': ['LO', '1', 'HumanPerformerOrganization'],
7641            '0x4037': ['PN', '1', 'HumanPerformerName'],
7642            '0x4040': ['CS', '1', 'RawDataHandling'],
7643            '0x4041': ['CS', '1', 'InputReadinessState'],
7644            '0x4050': ['DT', '1', 'PerformedProcedureStepStartDateTime'],
7645            '0x4051': ['DT', '1', 'PerformedProcedureStepEndDateTime'],
7646            '0x4052': ['DT', '1', 'ProcedureStepCancellationDateTime'],
7647            '0x8302': ['DS', '1', 'EntranceDoseInmGy'],
7648            '0x9092': ['SQ', '1', 'ParametricMapFrameTypeSequence'],
7649            '0x9094': ['SQ', '1', 'ReferencedImageRealWorldValueMappingSequence'],
7650            '0x9096': ['SQ', '1', 'RealWorldValueMappingSequence'],
7651            '0x9098': ['SQ', '1', 'PixelValueMappingCodeSequence'],
7652            '0x9210': ['SH', '1', 'LUTLabel'],
7653            '0x9211': ['xs', '1', 'RealWorldValueLastValueMapped'],
7654            '0x9212': ['FD', '1-n', 'RealWorldValueLUTData'],
7655            '0x9216': ['xs', '1', 'RealWorldValueFirstValueMapped'],
7656            '0x9220': ['SQ', '1', 'QuantityDefinitionSequence'],
7657            '0x9224': ['FD', '1', 'RealWorldValueIntercept'],
7658            '0x9225': ['FD', '1', 'RealWorldValueSlope'],
7659            '0xA007': ['CS', '1', 'FindingsFlagTrial'],
7660            '0xA010': ['CS', '1', 'RelationshipType'],
7661            '0xA020': ['SQ', '1', 'FindingsSequenceTrial'],
7662            '0xA021': ['UI', '1', 'FindingsGroupUIDTrial'],
7663            '0xA022': ['UI', '1', 'ReferencedFindingsGroupUIDTrial'],
7664            '0xA023': ['DA', '1', 'FindingsGroupRecordingDateTrial'],
7665            '0xA024': ['TM', '1', 'FindingsGroupRecordingTimeTrial'],
7666            '0xA026': ['SQ', '1', 'FindingsSourceCategoryCodeSequenceTrial'],
7667            '0xA027': ['LO', '1', 'VerifyingOrganization'],
7668            '0xA028': ['SQ', '1', 'DocumentingOrganizationIdentifierCodeSequenceTrial'],
7669            '0xA030': ['DT', '1', 'VerificationDateTime'],
7670            '0xA032': ['DT', '1', 'ObservationDateTime'],
7671            '0xA040': ['CS', '1', 'ValueType'],
7672            '0xA043': ['SQ', '1', 'ConceptNameCodeSequence'],
7673            '0xA047': ['LO', '1', 'MeasurementPrecisionDescriptionTrial'],
7674            '0xA050': ['CS', '1', 'ContinuityOfContent'],
7675            '0xA057': ['CS', '1-n', 'UrgencyOrPriorityAlertsTrial'],
7676            '0xA060': ['LO', '1', 'SequencingIndicatorTrial'],
7677            '0xA066': ['SQ', '1', 'DocumentIdentifierCodeSequenceTrial'],
7678            '0xA067': ['PN', '1', 'DocumentAuthorTrial'],
7679            '0xA068': ['SQ', '1', 'DocumentAuthorIdentifierCodeSequenceTrial'],
7680            '0xA070': ['SQ', '1', 'IdentifierCodeSequenceTrial'],
7681            '0xA073': ['SQ', '1', 'VerifyingObserverSequence'],
7682            '0xA074': ['OB', '1', 'ObjectBinaryIdentifierTrial'],
7683            '0xA075': ['PN', '1', 'VerifyingObserverName'],
7684            '0xA076': ['SQ', '1', 'DocumentingObserverIdentifierCodeSequenceTrial'],
7685            '0xA078': ['SQ', '1', 'AuthorObserverSequence'],
7686            '0xA07A': ['SQ', '1', 'ParticipantSequence'],
7687            '0xA07C': ['SQ', '1', 'CustodialOrganizationSequence'],
7688            '0xA080': ['CS', '1', 'ParticipationType'],
7689            '0xA082': ['DT', '1', 'ParticipationDateTime'],
7690            '0xA084': ['CS', '1', 'ObserverType'],
7691            '0xA085': ['SQ', '1', 'ProcedureIdentifierCodeSequenceTrial'],
7692            '0xA088': ['SQ', '1', 'VerifyingObserverIdentificationCodeSequence'],
7693            '0xA089': ['OB', '1', 'ObjectDirectoryBinaryIdentifierTrial'],
7694            '0xA090': ['SQ', '1', 'EquivalentCDADocumentSequence'],
7695            '0xA0B0': ['US', '2-2n', 'ReferencedWaveformChannels'],
7696            '0xA110': ['DA', '1', 'DateOfDocumentOrVerbalTransactionTrial'],
7697            '0xA112': ['TM', '1', 'TimeOfDocumentCreationOrVerbalTransactionTrial'],
7698            '0xA120': ['DT', '1', 'DateTime'],
7699            '0xA121': ['DA', '1', 'Date'],
7700            '0xA122': ['TM', '1', 'Time'],
7701            '0xA123': ['PN', '1', 'PersonName'],
7702            '0xA124': ['UI', '1', 'UID'],
7703            '0xA125': ['CS', '2', 'ReportStatusIDTrial'],
7704            '0xA130': ['CS', '1', 'TemporalRangeType'],
7705            '0xA132': ['UL', '1-n', 'ReferencedSamplePositions'],
7706            '0xA136': ['US', '1-n', 'ReferencedFrameNumbers'],
7707            '0xA138': ['DS', '1-n', 'ReferencedTimeOffsets'],
7708            '0xA13A': ['DT', '1-n', 'ReferencedDateTime'],
7709            '0xA160': ['UT', '1', 'TextValue'],
7710            '0xA161': ['FD', '1-n', 'FloatingPointValue'],
7711            '0xA162': ['SL', '1-n', 'RationalNumeratorValue'],
7712            '0xA163': ['UL', '1-n', 'RationalDenominatorValue'],
7713            '0xA167': ['SQ', '1', 'ObservationCategoryCodeSequenceTrial'],
7714            '0xA168': ['SQ', '1', 'ConceptCodeSequence'],
7715            '0xA16A': ['ST', '1', 'BibliographicCitationTrial'],
7716            '0xA170': ['SQ', '1', 'PurposeOfReferenceCodeSequence'],
7717            '0xA171': ['UI', '1', 'ObservationUID'],
7718            '0xA172': ['UI', '1', 'ReferencedObservationUIDTrial'],
7719            '0xA173': ['CS', '1', 'ReferencedObservationClassTrial'],
7720            '0xA174': ['CS', '1', 'ReferencedObjectObservationClassTrial'],
7721            '0xA180': ['US', '1', 'AnnotationGroupNumber'],
7722            '0xA192': ['DA', '1', 'ObservationDateTrial'],
7723            '0xA193': ['TM', '1', 'ObservationTimeTrial'],
7724            '0xA194': ['CS', '1', 'MeasurementAutomationTrial'],
7725            '0xA195': ['SQ', '1', 'ModifierCodeSequence'],
7726            '0xA224': ['ST', '1', 'IdentificationDescriptionTrial'],
7727            '0xA290': ['CS', '1', 'CoordinatesSetGeometricTypeTrial'],
7728            '0xA296': ['SQ', '1', 'AlgorithmCodeSequenceTrial'],
7729            '0xA297': ['ST', '1', 'AlgorithmDescriptionTrial'],
7730            '0xA29A': ['SL', '2-2n', 'PixelCoordinatesSetTrial'],
7731            '0xA300': ['SQ', '1', 'MeasuredValueSequence'],
7732            '0xA301': ['SQ', '1', 'NumericValueQualifierCodeSequence'],
7733            '0xA307': ['PN', '1', 'CurrentObserverTrial'],
7734            '0xA30A': ['DS', '1-n', 'NumericValue'],
7735            '0xA313': ['SQ', '1', 'ReferencedAccessionSequenceTrial'],
7736            '0xA33A': ['ST', '1', 'ReportStatusCommentTrial'],
7737            '0xA340': ['SQ', '1', 'ProcedureContextSequenceTrial'],
7738            '0xA352': ['PN', '1', 'VerbalSourceTrial'],
7739            '0xA353': ['ST', '1', 'AddressTrial'],
7740            '0xA354': ['LO', '1', 'TelephoneNumberTrial'],
7741            '0xA358': ['SQ', '1', 'VerbalSourceIdentifierCodeSequenceTrial'],
7742            '0xA360': ['SQ', '1', 'PredecessorDocumentsSequence'],
7743            '0xA370': ['SQ', '1', 'ReferencedRequestSequence'],
7744            '0xA372': ['SQ', '1', 'PerformedProcedureCodeSequence'],
7745            '0xA375': ['SQ', '1', 'CurrentRequestedProcedureEvidenceSequence'],
7746            '0xA380': ['SQ', '1', 'ReportDetailSequenceTrial'],
7747            '0xA385': ['SQ', '1', 'PertinentOtherEvidenceSequence'],
7748            '0xA390': ['SQ', '1', 'HL7StructuredDocumentReferenceSequence'],
7749            '0xA402': ['UI', '1', 'ObservationSubjectUIDTrial'],
7750            '0xA403': ['CS', '1', 'ObservationSubjectClassTrial'],
7751            '0xA404': ['SQ', '1', 'ObservationSubjectTypeCodeSequenceTrial'],
7752            '0xA491': ['CS', '1', 'CompletionFlag'],
7753            '0xA492': ['LO', '1', 'CompletionFlagDescription'],
7754            '0xA493': ['CS', '1', 'VerificationFlag'],
7755            '0xA494': ['CS', '1', 'ArchiveRequested'],
7756            '0xA496': ['CS', '1', 'PreliminaryFlag'],
7757            '0xA504': ['SQ', '1', 'ContentTemplateSequence'],
7758            '0xA525': ['SQ', '1', 'IdenticalDocumentsSequence'],
7759            '0xA600': ['CS', '1', 'ObservationSubjectContextFlagTrial'],
7760            '0xA601': ['CS', '1', 'ObserverContextFlagTrial'],
7761            '0xA603': ['CS', '1', 'ProcedureContextFlagTrial'],
7762            '0xA730': ['SQ', '1', 'ContentSequence'],
7763            '0xA731': ['SQ', '1', 'RelationshipSequenceTrial'],
7764            '0xA732': ['SQ', '1', 'RelationshipTypeCodeSequenceTrial'],
7765            '0xA744': ['SQ', '1', 'LanguageCodeSequenceTrial'],
7766            '0xA992': ['ST', '1', 'UniformResourceLocatorTrial'],
7767            '0xB020': ['SQ', '1', 'WaveformAnnotationSequence'],
7768            '0xDB00': ['CS', '1', 'TemplateIdentifier'],
7769            '0xDB06': ['DT', '1', 'TemplateVersion'],
7770            '0xDB07': ['DT', '1', 'TemplateLocalVersion'],
7771            '0xDB0B': ['CS', '1', 'TemplateExtensionFlag'],
7772            '0xDB0C': ['UI', '1', 'TemplateExtensionOrganizationUID'],
7773            '0xDB0D': ['UI', '1', 'TemplateExtensionCreatorUID'],
7774            '0xDB73': ['UL', '1-n', 'ReferencedContentItemIdentifier'],
7775            '0xE001': ['ST', '1', 'HL7InstanceIdentifier'],
7776            '0xE004': ['DT', '1', 'HL7DocumentEffectiveTime'],
7777            '0xE006': ['SQ', '1', 'HL7DocumentTypeCodeSequence'],
7778            '0xE008': ['SQ', '1', 'DocumentClassCodeSequence'],
7779            '0xE010': ['UR', '1', 'RetrieveURI'],
7780            '0xE011': ['UI', '1', 'RetrieveLocationUID'],
7781            '0xE020': ['CS', '1', 'TypeOfInstances'],
7782            '0xE021': ['SQ', '1', 'DICOMRetrievalSequence'],
7783            '0xE022': ['SQ', '1', 'DICOMMediaRetrievalSequence'],
7784            '0xE023': ['SQ', '1', 'WADORetrievalSequence'],
7785            '0xE024': ['SQ', '1', 'XDSRetrievalSequence'],
7786            '0xE025': ['SQ', '1', 'WADORSRetrievalSequence'],
7787            '0xE030': ['UI', '1', 'RepositoryUniqueID'],
7788            '0xE031': ['UI', '1', 'HomeCommunityID']
7789        },
7790        '0x0042': {
7791            '0x0000': ['UL', '1', 'GenericGroupLength'],
7792            '0x0010': ['ST', '1', 'DocumentTitle'],
7793            '0x0011': ['OB', '1', 'EncapsulatedDocument'],
7794            '0x0012': ['LO', '1', 'MIMETypeOfEncapsulatedDocument'],
7795            '0x0013': ['SQ', '1', 'SourceInstanceSequence'],
7796            '0x0014': ['LO', '1-n', 'ListOfMIMETypes']
7797        },
7798        '0x0044': {
7799            '0x0000': ['UL', '1', 'GenericGroupLength'],
7800            '0x0001': ['ST', '1', 'ProductPackageIdentifier'],
7801            '0x0002': ['CS', '1', 'SubstanceAdministrationApproval'],
7802            '0x0003': ['LT', '1', 'ApprovalStatusFurtherDescription'],
7803            '0x0004': ['DT', '1', 'ApprovalStatusDateTime'],
7804            '0x0007': ['SQ', '1', 'ProductTypeCodeSequence'],
7805            '0x0008': ['LO', '1-n', 'ProductName'],
7806            '0x0009': ['LT', '1', 'ProductDescription'],
7807            '0x000A': ['LO', '1', 'ProductLotIdentifier'],
7808            '0x000B': ['DT', '1', 'ProductExpirationDateTime'],
7809            '0x0010': ['DT', '1', 'SubstanceAdministrationDateTime'],
7810            '0x0011': ['LO', '1', 'SubstanceAdministrationNotes'],
7811            '0x0012': ['LO', '1', 'SubstanceAdministrationDeviceID'],
7812            '0x0013': ['SQ', '1', 'ProductParameterSequence'],
7813            '0x0019': ['SQ', '1', 'SubstanceAdministrationParameterSequence']
7814        },
7815        '0x0046': {
7816            '0x0000': ['UL', '1', 'GenericGroupLength'],
7817            '0x0012': ['LO', '1', 'LensDescription'],
7818            '0x0014': ['SQ', '1', 'RightLensSequence'],
7819            '0x0015': ['SQ', '1', 'LeftLensSequence'],
7820            '0x0016': ['SQ', '1', 'UnspecifiedLateralityLensSequence'],
7821            '0x0018': ['SQ', '1', 'CylinderSequence'],
7822            '0x0028': ['SQ', '1', 'PrismSequence'],
7823            '0x0030': ['FD', '1', 'HorizontalPrismPower'],
7824            '0x0032': ['CS', '1', 'HorizontalPrismBase'],
7825            '0x0034': ['FD', '1', 'VerticalPrismPower'],
7826            '0x0036': ['CS', '1', 'VerticalPrismBase'],
7827            '0x0038': ['CS', '1', 'LensSegmentType'],
7828            '0x0040': ['FD', '1', 'OpticalTransmittance'],
7829            '0x0042': ['FD', '1', 'ChannelWidth'],
7830            '0x0044': ['FD', '1', 'PupilSize'],
7831            '0x0046': ['FD', '1', 'CornealSize'],
7832            '0x0050': ['SQ', '1', 'AutorefractionRightEyeSequence'],
7833            '0x0052': ['SQ', '1', 'AutorefractionLeftEyeSequence'],
7834            '0x0060': ['FD', '1', 'DistancePupillaryDistance'],
7835            '0x0062': ['FD', '1', 'NearPupillaryDistance'],
7836            '0x0063': ['FD', '1', 'IntermediatePupillaryDistance'],
7837            '0x0064': ['FD', '1', 'OtherPupillaryDistance'],
7838            '0x0070': ['SQ', '1', 'KeratometryRightEyeSequence'],
7839            '0x0071': ['SQ', '1', 'KeratometryLeftEyeSequence'],
7840            '0x0074': ['SQ', '1', 'SteepKeratometricAxisSequence'],
7841            '0x0075': ['FD', '1', 'RadiusOfCurvature'],
7842            '0x0076': ['FD', '1', 'KeratometricPower'],
7843            '0x0077': ['FD', '1', 'KeratometricAxis'],
7844            '0x0080': ['SQ', '1', 'FlatKeratometricAxisSequence'],
7845            '0x0092': ['CS', '1', 'BackgroundColor'],
7846            '0x0094': ['CS', '1', 'Optotype'],
7847            '0x0095': ['CS', '1', 'OptotypePresentation'],
7848            '0x0097': ['SQ', '1', 'SubjectiveRefractionRightEyeSequence'],
7849            '0x0098': ['SQ', '1', 'SubjectiveRefractionLeftEyeSequence'],
7850            '0x0100': ['SQ', '1', 'AddNearSequence'],
7851            '0x0101': ['SQ', '1', 'AddIntermediateSequence'],
7852            '0x0102': ['SQ', '1', 'AddOtherSequence'],
7853            '0x0104': ['FD', '1', 'AddPower'],
7854            '0x0106': ['FD', '1', 'ViewingDistance'],
7855            '0x0121': ['SQ', '1', 'VisualAcuityTypeCodeSequence'],
7856            '0x0122': ['SQ', '1', 'VisualAcuityRightEyeSequence'],
7857            '0x0123': ['SQ', '1', 'VisualAcuityLeftEyeSequence'],
7858            '0x0124': ['SQ', '1', 'VisualAcuityBothEyesOpenSequence'],
7859            '0x0125': ['CS', '1', 'ViewingDistanceType'],
7860            '0x0135': ['SS', '2', 'VisualAcuityModifiers'],
7861            '0x0137': ['FD', '1', 'DecimalVisualAcuity'],
7862            '0x0139': ['LO', '1', 'OptotypeDetailedDefinition'],
7863            '0x0145': ['SQ', '1', 'ReferencedRefractiveMeasurementsSequence'],
7864            '0x0146': ['FD', '1', 'SpherePower'],
7865            '0x0147': ['FD', '1', 'CylinderPower'],
7866            '0x0201': ['CS', '1', 'CornealTopographySurface'],
7867            '0x0202': ['FL', '2', 'CornealVertexLocation'],
7868            '0x0203': ['FL', '1', 'PupilCentroidXCoordinate'],
7869            '0x0204': ['FL', '1', 'PupilCentroidYCoordinate'],
7870            '0x0205': ['FL', '1', 'EquivalentPupilRadius'],
7871            '0x0207': ['SQ', '1', 'CornealTopographyMapTypeCodeSequence'],
7872            '0x0208': ['IS', '2-2n', 'VerticesOfTheOutlineOfPupil'],
7873            '0x0210': ['SQ', '1', 'CornealTopographyMappingNormalsSequence'],
7874            '0x0211': ['SQ', '1', 'MaximumCornealCurvatureSequence'],
7875            '0x0212': ['FL', '1', 'MaximumCornealCurvature'],
7876            '0x0213': ['FL', '2', 'MaximumCornealCurvatureLocation'],
7877            '0x0215': ['SQ', '1', 'MinimumKeratometricSequence'],
7878            '0x0218': ['SQ', '1', 'SimulatedKeratometricCylinderSequence'],
7879            '0x0220': ['FL', '1', 'AverageCornealPower'],
7880            '0x0224': ['FL', '1', 'CornealISValue'],
7881            '0x0227': ['FL', '1', 'AnalyzedArea'],
7882            '0x0230': ['FL', '1', 'SurfaceRegularityIndex'],
7883            '0x0232': ['FL', '1', 'SurfaceAsymmetryIndex'],
7884            '0x0234': ['FL', '1', 'CornealEccentricityIndex'],
7885            '0x0236': ['FL', '1', 'KeratoconusPredictionIndex'],
7886            '0x0238': ['FL', '1', 'DecimalPotentialVisualAcuity'],
7887            '0x0242': ['CS', '1', 'CornealTopographyMapQualityEvaluation'],
7888            '0x0244': ['SQ', '1', 'SourceImageCornealProcessedDataSequence'],
7889            '0x0247': ['FL', '3', 'CornealPointLocation'],
7890            '0x0248': ['CS', '1', 'CornealPointEstimated'],
7891            '0x0249': ['FL', '1', 'AxialPower'],
7892            '0x0250': ['FL', '1', 'TangentialPower'],
7893            '0x0251': ['FL', '1', 'RefractivePower'],
7894            '0x0252': ['FL', '1', 'RelativeElevation'],
7895            '0x0253': ['FL', '1', 'CornealWavefront']
7896        },
7897        '0x0048': {
7898            '0x0000': ['UL', '1', 'GenericGroupLength'],
7899            '0x0001': ['FL', '1', 'ImagedVolumeWidth'],
7900            '0x0002': ['FL', '1', 'ImagedVolumeHeight'],
7901            '0x0003': ['FL', '1', 'ImagedVolumeDepth'],
7902            '0x0006': ['UL', '1', 'TotalPixelMatrixColumns'],
7903            '0x0007': ['UL', '1', 'TotalPixelMatrixRows'],
7904            '0x0008': ['SQ', '1', 'TotalPixelMatrixOriginSequence'],
7905            '0x0010': ['CS', '1', 'SpecimenLabelInImage'],
7906            '0x0011': ['CS', '1', 'FocusMethod'],
7907            '0x0012': ['CS', '1', 'ExtendedDepthOfField'],
7908            '0x0013': ['US', '1', 'NumberOfFocalPlanes'],
7909            '0x0014': ['FL', '1', 'DistanceBetweenFocalPlanes'],
7910            '0x0015': ['US', '3', 'RecommendedAbsentPixelCIELabValue'],
7911            '0x0100': ['SQ', '1', 'IlluminatorTypeCodeSequence'],
7912            '0x0102': ['DS', '6', 'ImageOrientationSlide'],
7913            '0x0105': ['SQ', '1', 'OpticalPathSequence'],
7914            '0x0106': ['SH', '1', 'OpticalPathIdentifier'],
7915            '0x0107': ['ST', '1', 'OpticalPathDescription'],
7916            '0x0108': ['SQ', '1', 'IlluminationColorCodeSequence'],
7917            '0x0110': ['SQ', '1', 'SpecimenReferenceSequence'],
7918            '0x0111': ['DS', '1', 'CondenserLensPower'],
7919            '0x0112': ['DS', '1', 'ObjectiveLensPower'],
7920            '0x0113': ['DS', '1', 'ObjectiveLensNumericalAperture'],
7921            '0x0120': ['SQ', '1', 'PaletteColorLookupTableSequence'],
7922            '0x0200': ['SQ', '1', 'ReferencedImageNavigationSequence'],
7923            '0x0201': ['US', '2', 'TopLeftHandCornerOfLocalizerArea'],
7924            '0x0202': ['US', '2', 'BottomRightHandCornerOfLocalizerArea'],
7925            '0x0207': ['SQ', '1', 'OpticalPathIdentificationSequence'],
7926            '0x021A': ['SQ', '1', 'PlanePositionSlideSequence'],
7927            '0x021E': ['SL', '1', 'ColumnPositionInTotalImagePixelMatrix'],
7928            '0x021F': ['SL', '1', 'RowPositionInTotalImagePixelMatrix'],
7929            '0x0301': ['CS', '1', 'PixelOriginInterpretation']
7930        },
7931        '0x0050': {
7932            '0x0000': ['UL', '1', 'GenericGroupLength'],
7933            '0x0004': ['CS', '1', 'CalibrationImage'],
7934            '0x0010': ['SQ', '1', 'DeviceSequence'],
7935            '0x0012': ['SQ', '1', 'ContainerComponentTypeCodeSequence'],
7936            '0x0013': ['FD', '1', 'ContainerComponentThickness'],
7937            '0x0014': ['DS', '1', 'DeviceLength'],
7938            '0x0015': ['FD', '1', 'ContainerComponentWidth'],
7939            '0x0016': ['DS', '1', 'DeviceDiameter'],
7940            '0x0017': ['CS', '1', 'DeviceDiameterUnits'],
7941            '0x0018': ['DS', '1', 'DeviceVolume'],
7942            '0x0019': ['DS', '1', 'InterMarkerDistance'],
7943            '0x001A': ['CS', '1', 'ContainerComponentMaterial'],
7944            '0x001B': ['LO', '1', 'ContainerComponentID'],
7945            '0x001C': ['FD', '1', 'ContainerComponentLength'],
7946            '0x001D': ['FD', '1', 'ContainerComponentDiameter'],
7947            '0x001E': ['LO', '1', 'ContainerComponentDescription'],
7948            '0x0020': ['LO', '1', 'DeviceDescription']
7949        },
7950        '0x0052': {
7951            '0x0000': ['UL', '1', 'GenericGroupLength'],
7952            '0x0001': ['FL', '1', 'ContrastBolusIngredientPercentByVolume'],
7953            '0x0002': ['FD', '1', 'OCTFocalDistance'],
7954            '0x0003': ['FD', '1', 'BeamSpotSize'],
7955            '0x0004': ['FD', '1', 'EffectiveRefractiveIndex'],
7956            '0x0006': ['CS', '1', 'OCTAcquisitionDomain'],
7957            '0x0007': ['FD', '1', 'OCTOpticalCenterWavelength'],
7958            '0x0008': ['FD', '1', 'AxialResolution'],
7959            '0x0009': ['FD', '1', 'RangingDepth'],
7960            '0x0011': ['FD', '1', 'ALineRate'],
7961            '0x0012': ['US', '1', 'ALinesPerFrame'],
7962            '0x0013': ['FD', '1', 'CatheterRotationalRate'],
7963            '0x0014': ['FD', '1', 'ALinePixelSpacing'],
7964            '0x0016': ['SQ', '1', 'ModeOfPercutaneousAccessSequence'],
7965            '0x0025': ['SQ', '1', 'IntravascularOCTFrameTypeSequence'],
7966            '0x0026': ['CS', '1', 'OCTZOffsetApplied'],
7967            '0x0027': ['SQ', '1', 'IntravascularFrameContentSequence'],
7968            '0x0028': ['FD', '1', 'IntravascularLongitudinalDistance'],
7969            '0x0029': ['SQ', '1', 'IntravascularOCTFrameContentSequence'],
7970            '0x0030': ['SS', '1', 'OCTZOffsetCorrection'],
7971            '0x0031': ['CS', '1', 'CatheterDirectionOfRotation'],
7972            '0x0033': ['FD', '1', 'SeamLineLocation'],
7973            '0x0034': ['FD', '1', 'FirstALineLocation'],
7974            '0x0036': ['US', '1', 'SeamLineIndex'],
7975            '0x0038': ['US', '1', 'NumberOfPaddedALines'],
7976            '0x0039': ['CS', '1', 'InterpolationType'],
7977            '0x003A': ['CS', '1', 'RefractiveIndexApplied']
7978        },
7979        '0x0054': {
7980            '0x0000': ['UL', '1', 'GenericGroupLength'],
7981            '0x0010': ['US', '1-n', 'EnergyWindowVector'],
7982            '0x0011': ['US', '1', 'NumberOfEnergyWindows'],
7983            '0x0012': ['SQ', '1', 'EnergyWindowInformationSequence'],
7984            '0x0013': ['SQ', '1', 'EnergyWindowRangeSequence'],
7985            '0x0014': ['DS', '1', 'EnergyWindowLowerLimit'],
7986            '0x0015': ['DS', '1', 'EnergyWindowUpperLimit'],
7987            '0x0016': ['SQ', '1', 'RadiopharmaceuticalInformationSequence'],
7988            '0x0017': ['IS', '1', 'ResidualSyringeCounts'],
7989            '0x0018': ['SH', '1', 'EnergyWindowName'],
7990            '0x0020': ['US', '1-n', 'DetectorVector'],
7991            '0x0021': ['US', '1', 'NumberOfDetectors'],
7992            '0x0022': ['SQ', '1', 'DetectorInformationSequence'],
7993            '0x0030': ['US', '1-n', 'PhaseVector'],
7994            '0x0031': ['US', '1', 'NumberOfPhases'],
7995            '0x0032': ['SQ', '1', 'PhaseInformationSequence'],
7996            '0x0033': ['US', '1', 'NumberOfFramesInPhase'],
7997            '0x0036': ['IS', '1', 'PhaseDelay'],
7998            '0x0038': ['IS', '1', 'PauseBetweenFrames'],
7999            '0x0039': ['CS', '1', 'PhaseDescription'],
8000            '0x0050': ['US', '1-n', 'RotationVector'],
8001            '0x0051': ['US', '1', 'NumberOfRotations'],
8002            '0x0052': ['SQ', '1', 'RotationInformationSequence'],
8003            '0x0053': ['US', '1', 'NumberOfFramesInRotation'],
8004            '0x0060': ['US', '1-n', 'RRIntervalVector'],
8005            '0x0061': ['US', '1', 'NumberOfRRIntervals'],
8006            '0x0062': ['SQ', '1', 'GatedInformationSequence'],
8007            '0x0063': ['SQ', '1', 'DataInformationSequence'],
8008            '0x0070': ['US', '1-n', 'TimeSlotVector'],
8009            '0x0071': ['US', '1', 'NumberOfTimeSlots'],
8010            '0x0072': ['SQ', '1', 'TimeSlotInformationSequence'],
8011            '0x0073': ['DS', '1', 'TimeSlotTime'],
8012            '0x0080': ['US', '1-n', 'SliceVector'],
8013            '0x0081': ['US', '1', 'NumberOfSlices'],
8014            '0x0090': ['US', '1-n', 'AngularViewVector'],
8015            '0x0100': ['US', '1-n', 'TimeSliceVector'],
8016            '0x0101': ['US', '1', 'NumberOfTimeSlices'],
8017            '0x0200': ['DS', '1', 'StartAngle'],
8018            '0x0202': ['CS', '1', 'TypeOfDetectorMotion'],
8019            '0x0210': ['IS', '1-n', 'TriggerVector'],
8020            '0x0211': ['US', '1', 'NumberOfTriggersInPhase'],
8021            '0x0220': ['SQ', '1', 'ViewCodeSequence'],
8022            '0x0222': ['SQ', '1', 'ViewModifierCodeSequence'],
8023            '0x0300': ['SQ', '1', 'RadionuclideCodeSequence'],
8024            '0x0302': ['SQ', '1', 'AdministrationRouteCodeSequence'],
8025            '0x0304': ['SQ', '1', 'RadiopharmaceuticalCodeSequence'],
8026            '0x0306': ['SQ', '1', 'CalibrationDataSequence'],
8027            '0x0308': ['US', '1', 'EnergyWindowNumber'],
8028            '0x0400': ['SH', '1', 'ImageID'],
8029            '0x0410': ['SQ', '1', 'PatientOrientationCodeSequence'],
8030            '0x0412': ['SQ', '1', 'PatientOrientationModifierCodeSequence'],
8031            '0x0414': ['SQ', '1', 'PatientGantryRelationshipCodeSequence'],
8032            '0x0500': ['CS', '1', 'SliceProgressionDirection'],
8033            '0x0501': ['CS', '1', 'ScanProgressionDirection'],
8034            '0x1000': ['CS', '2', 'SeriesType'],
8035            '0x1001': ['CS', '1', 'Units'],
8036            '0x1002': ['CS', '1', 'CountsSource'],
8037            '0x1004': ['CS', '1', 'ReprojectionMethod'],
8038            '0x1006': ['CS', '1', 'SUVType'],
8039            '0x1100': ['CS', '1', 'RandomsCorrectionMethod'],
8040            '0x1101': ['LO', '1', 'AttenuationCorrectionMethod'],
8041            '0x1102': ['CS', '1', 'DecayCorrection'],
8042            '0x1103': ['LO', '1', 'ReconstructionMethod'],
8043            '0x1104': ['LO', '1', 'DetectorLinesOfResponseUsed'],
8044            '0x1105': ['LO', '1', 'ScatterCorrectionMethod'],
8045            '0x1200': ['DS', '1', 'AxialAcceptance'],
8046            '0x1201': ['IS', '2', 'AxialMash'],
8047            '0x1202': ['IS', '1', 'TransverseMash'],
8048            '0x1203': ['DS', '2', 'DetectorElementSize'],
8049            '0x1210': ['DS', '1', 'CoincidenceWindowWidth'],
8050            '0x1220': ['CS', '1-n', 'SecondaryCountsType'],
8051            '0x1300': ['DS', '1', 'FrameReferenceTime'],
8052            '0x1310': ['IS', '1', 'PrimaryPromptsCountsAccumulated'],
8053            '0x1311': ['IS', '1-n', 'SecondaryCountsAccumulated'],
8054            '0x1320': ['DS', '1', 'SliceSensitivityFactor'],
8055            '0x1321': ['DS', '1', 'DecayFactor'],
8056            '0x1322': ['DS', '1', 'DoseCalibrationFactor'],
8057            '0x1323': ['DS', '1', 'ScatterFractionFactor'],
8058            '0x1324': ['DS', '1', 'DeadTimeFactor'],
8059            '0x1330': ['US', '1', 'ImageIndex'],
8060            '0x1400': ['CS', '1-n', 'CountsIncluded'],
8061            '0x1401': ['CS', '1', 'DeadTimeCorrectionFlag']
8062        },
8063        '0x0060': {
8064            '0x0000': ['UL', '1', 'GenericGroupLength'],
8065            '0x3000': ['SQ', '1', 'HistogramSequence'],
8066            '0x3002': ['US', '1', 'HistogramNumberOfBins'],
8067            '0x3004': ['xs', '1', 'HistogramFirstBinValue'],
8068            '0x3006': ['xs', '1', 'HistogramLastBinValue'],
8069            '0x3008': ['US', '1', 'HistogramBinWidth'],
8070            '0x3010': ['LO', '1', 'HistogramExplanation'],
8071            '0x3020': ['UL', '1-n', 'HistogramData']
8072        },
8073        '0x0062': {
8074            '0x0000': ['UL', '1', 'GenericGroupLength'],
8075            '0x0001': ['CS', '1', 'SegmentationType'],
8076            '0x0002': ['SQ', '1', 'SegmentSequence'],
8077            '0x0003': ['SQ', '1', 'SegmentedPropertyCategoryCodeSequence'],
8078            '0x0004': ['US', '1', 'SegmentNumber'],
8079            '0x0005': ['LO', '1', 'SegmentLabel'],
8080            '0x0006': ['ST', '1', 'SegmentDescription'],
8081            '0x0008': ['CS', '1', 'SegmentAlgorithmType'],
8082            '0x0009': ['LO', '1', 'SegmentAlgorithmName'],
8083            '0x000A': ['SQ', '1', 'SegmentIdentificationSequence'],
8084            '0x000B': ['US', '1-n', 'ReferencedSegmentNumber'],
8085            '0x000C': ['US', '1', 'RecommendedDisplayGrayscaleValue'],
8086            '0x000D': ['US', '3', 'RecommendedDisplayCIELabValue'],
8087            '0x000E': ['US', '1', 'MaximumFractionalValue'],
8088            '0x000F': ['SQ', '1', 'SegmentedPropertyTypeCodeSequence'],
8089            '0x0010': ['CS', '1', 'SegmentationFractionalType'],
8090            '0x0011': ['SQ', '1', 'SegmentedPropertyTypeModifierCodeSequence'],
8091            '0x0012': ['SQ', '1', 'UsedSegmentsSequence']
8092        },
8093        '0x0064': {
8094            '0x0000': ['UL', '1', 'GenericGroupLength'],
8095            '0x0002': ['SQ', '1', 'DeformableRegistrationSequence'],
8096            '0x0003': ['UI', '1', 'SourceFrameOfReferenceUID'],
8097            '0x0005': ['SQ', '1', 'DeformableRegistrationGridSequence'],
8098            '0x0007': ['UL', '3', 'GridDimensions'],
8099            '0x0008': ['FD', '3', 'GridResolution'],
8100            '0x0009': ['OF', '1', 'VectorGridData'],
8101            '0x000F': ['SQ', '1', 'PreDeformationMatrixRegistrationSequence'],
8102            '0x0010': ['SQ', '1', 'PostDeformationMatrixRegistrationSequence']
8103        },
8104        '0x0066': {
8105            '0x0000': ['UL', '1', 'GenericGroupLength'],
8106            '0x0001': ['UL', '1', 'NumberOfSurfaces'],
8107            '0x0002': ['SQ', '1', 'SurfaceSequence'],
8108            '0x0003': ['UL', '1', 'SurfaceNumber'],
8109            '0x0004': ['LT', '1', 'SurfaceComments'],
8110            '0x0009': ['CS', '1', 'SurfaceProcessing'],
8111            '0x000A': ['FL', '1', 'SurfaceProcessingRatio'],
8112            '0x000B': ['LO', '1', 'SurfaceProcessingDescription'],
8113            '0x000C': ['FL', '1', 'RecommendedPresentationOpacity'],
8114            '0x000D': ['CS', '1', 'RecommendedPresentationType'],
8115            '0x000E': ['CS', '1', 'FiniteVolume'],
8116            '0x0010': ['CS', '1', 'Manifold'],
8117            '0x0011': ['SQ', '1', 'SurfacePointsSequence'],
8118            '0x0012': ['SQ', '1', 'SurfacePointsNormalsSequence'],
8119            '0x0013': ['SQ', '1', 'SurfaceMeshPrimitivesSequence'],
8120            '0x0015': ['UL', '1', 'NumberOfSurfacePoints'],
8121            '0x0016': ['OF', '1', 'PointCoordinatesData'],
8122            '0x0017': ['FL', '3', 'PointPositionAccuracy'],
8123            '0x0018': ['FL', '1', 'MeanPointDistance'],
8124            '0x0019': ['FL', '1', 'MaximumPointDistance'],
8125            '0x001A': ['FL', '6', 'PointsBoundingBoxCoordinates'],
8126            '0x001B': ['FL', '3', 'AxisOfRotation'],
8127            '0x001C': ['FL', '3', 'CenterOfRotation'],
8128            '0x001E': ['UL', '1', 'NumberOfVectors'],
8129            '0x001F': ['US', '1', 'VectorDimensionality'],
8130            '0x0020': ['FL', '1-n', 'VectorAccuracy'],
8131            '0x0021': ['OF', '1', 'VectorCoordinateData'],
8132            '0x0023': ['OW', '1', 'TrianglePointIndexList'],
8133            '0x0024': ['OW', '1', 'EdgePointIndexList'],
8134            '0x0025': ['OW', '1', 'VertexPointIndexList'],
8135            '0x0026': ['SQ', '1', 'TriangleStripSequence'],
8136            '0x0027': ['SQ', '1', 'TriangleFanSequence'],
8137            '0x0028': ['SQ', '1', 'LineSequence'],
8138            '0x0029': ['OW', '1', 'PrimitivePointIndexList'],
8139            '0x002A': ['UL', '1', 'SurfaceCount'],
8140            '0x002B': ['SQ', '1', 'ReferencedSurfaceSequence'],
8141            '0x002C': ['UL', '1', 'ReferencedSurfaceNumber'],
8142            '0x002D': ['SQ', '1', 'SegmentSurfaceGenerationAlgorithmIdentificationSequence'],
8143            '0x002E': ['SQ', '1', 'SegmentSurfaceSourceInstanceSequence'],
8144            '0x002F': ['SQ', '1', 'AlgorithmFamilyCodeSequence'],
8145            '0x0030': ['SQ', '1', 'AlgorithmNameCodeSequence'],
8146            '0x0031': ['LO', '1', 'AlgorithmVersion'],
8147            '0x0032': ['LT', '1', 'AlgorithmParameters'],
8148            '0x0034': ['SQ', '1', 'FacetSequence'],
8149            '0x0035': ['SQ', '1', 'SurfaceProcessingAlgorithmIdentificationSequence'],
8150            '0x0036': ['LO', '1', 'AlgorithmName'],
8151            '0x0037': ['FL', '1', 'RecommendedPointRadius'],
8152            '0x0038': ['FL', '1', 'RecommendedLineThickness'],
8153            '0x0040': ['UL', '1-n', 'LongPrimitivePointIndexList'],
8154            '0x0041': ['UL', '3-3n', 'LongTrianglePointIndexList'],
8155            '0x0042': ['UL', '2-2n', 'LongEdgePointIndexList'],
8156            '0x0043': ['UL', '1-n', 'LongVertexPointIndexList']
8157        },
8158        '0x0068': {
8159            '0x0000': ['UL', '1', 'GenericGroupLength'],
8160            '0x6210': ['LO', '1', 'ImplantSize'],
8161            '0x6221': ['LO', '1', 'ImplantTemplateVersion'],
8162            '0x6222': ['SQ', '1', 'ReplacedImplantTemplateSequence'],
8163            '0x6223': ['CS', '1', 'ImplantType'],
8164            '0x6224': ['SQ', '1', 'DerivationImplantTemplateSequence'],
8165            '0x6225': ['SQ', '1', 'OriginalImplantTemplateSequence'],
8166            '0x6226': ['DT', '1', 'EffectiveDateTime'],
8167            '0x6230': ['SQ', '1', 'ImplantTargetAnatomySequence'],
8168            '0x6260': ['SQ', '1', 'InformationFromManufacturerSequence'],
8169            '0x6265': ['SQ', '1', 'NotificationFromManufacturerSequence'],
8170            '0x6270': ['DT', '1', 'InformationIssueDateTime'],
8171            '0x6280': ['ST', '1', 'InformationSummary'],
8172            '0x62A0': ['SQ', '1', 'ImplantRegulatoryDisapprovalCodeSequence'],
8173            '0x62A5': ['FD', '1', 'OverallTemplateSpatialTolerance'],
8174            '0x62C0': ['SQ', '1', 'HPGLDocumentSequence'],
8175            '0x62D0': ['US', '1', 'HPGLDocumentID'],
8176            '0x62D5': ['LO', '1', 'HPGLDocumentLabel'],
8177            '0x62E0': ['SQ', '1', 'ViewOrientationCodeSequence'],
8178            '0x62F0': ['FD', '9', 'ViewOrientationModifier'],
8179            '0x62F2': ['FD', '1', 'HPGLDocumentScaling'],
8180            '0x6300': ['OB', '1', 'HPGLDocument'],
8181            '0x6310': ['US', '1', 'HPGLContourPenNumber'],
8182            '0x6320': ['SQ', '1', 'HPGLPenSequence'],
8183            '0x6330': ['US', '1', 'HPGLPenNumber'],
8184            '0x6340': ['LO', '1', 'HPGLPenLabel'],
8185            '0x6345': ['ST', '1', 'HPGLPenDescription'],
8186            '0x6346': ['FD', '2', 'RecommendedRotationPoint'],
8187            '0x6347': ['FD', '4', 'BoundingRectangle'],
8188            '0x6350': ['US', '1-n', 'ImplantTemplate3DModelSurfaceNumber'],
8189            '0x6360': ['SQ', '1', 'SurfaceModelDescriptionSequence'],
8190            '0x6380': ['LO', '1', 'SurfaceModelLabel'],
8191            '0x6390': ['FD', '1', 'SurfaceModelScalingFactor'],
8192            '0x63A0': ['SQ', '1', 'MaterialsCodeSequence'],
8193            '0x63A4': ['SQ', '1', 'CoatingMaterialsCodeSequence'],
8194            '0x63A8': ['SQ', '1', 'ImplantTypeCodeSequence'],
8195            '0x63AC': ['SQ', '1', 'FixationMethodCodeSequence'],
8196            '0x63B0': ['SQ', '1', 'MatingFeatureSetsSequence'],
8197            '0x63C0': ['US', '1', 'MatingFeatureSetID'],
8198            '0x63D0': ['LO', '1', 'MatingFeatureSetLabel'],
8199            '0x63E0': ['SQ', '1', 'MatingFeatureSequence'],
8200            '0x63F0': ['US', '1', 'MatingFeatureID'],
8201            '0x6400': ['SQ', '1', 'MatingFeatureDegreeOfFreedomSequence'],
8202            '0x6410': ['US', '1', 'DegreeOfFreedomID'],
8203            '0x6420': ['CS', '1', 'DegreeOfFreedomType'],
8204            '0x6430': ['SQ', '1', 'TwoDMatingFeatureCoordinatesSequence'],
8205            '0x6440': ['US', '1', 'ReferencedHPGLDocumentID'],
8206            '0x6450': ['FD', '2', 'TwoDMatingPoint'],
8207            '0x6460': ['FD', '4', 'TwoDMatingAxes'],
8208            '0x6470': ['SQ', '1', 'TwoDDegreeOfFreedomSequence'],
8209            '0x6490': ['FD', '3', 'ThreeDDegreeOfFreedomAxis'],
8210            '0x64A0': ['FD', '2', 'RangeOfFreedom'],
8211            '0x64C0': ['FD', '3', 'ThreeDMatingPoint'],
8212            '0x64D0': ['FD', '9', 'ThreeDMatingAxes'],
8213            '0x64F0': ['FD', '3', 'TwoDDegreeOfFreedomAxis'],
8214            '0x6500': ['SQ', '1', 'PlanningLandmarkPointSequence'],
8215            '0x6510': ['SQ', '1', 'PlanningLandmarkLineSequence'],
8216            '0x6520': ['SQ', '1', 'PlanningLandmarkPlaneSequence'],
8217            '0x6530': ['US', '1', 'PlanningLandmarkID'],
8218            '0x6540': ['LO', '1', 'PlanningLandmarkDescription'],
8219            '0x6545': ['SQ', '1', 'PlanningLandmarkIdentificationCodeSequence'],
8220            '0x6550': ['SQ', '1', 'TwoDPointCoordinatesSequence'],
8221            '0x6560': ['FD', '2', 'TwoDPointCoordinates'],
8222            '0x6590': ['FD', '3', 'ThreeDPointCoordinates'],
8223            '0x65A0': ['SQ', '1', 'TwoDLineCoordinatesSequence'],
8224            '0x65B0': ['FD', '4', 'TwoDLineCoordinates'],
8225            '0x65D0': ['FD', '6', 'ThreeDLineCoordinates'],
8226            '0x65E0': ['SQ', '1', 'TwoDPlaneCoordinatesSequence'],
8227            '0x65F0': ['FD', '4', 'TwoDPlaneIntersection'],
8228            '0x6610': ['FD', '3', 'ThreeDPlaneOrigin'],
8229            '0x6620': ['FD', '3', 'ThreeDPlaneNormal']
8230        },
8231        '0x0070': {
8232            '0x0000': ['UL', '1', 'GenericGroupLength'],
8233            '0x0001': ['SQ', '1', 'GraphicAnnotationSequence'],
8234            '0x0002': ['CS', '1', 'GraphicLayer'],
8235            '0x0003': ['CS', '1', 'BoundingBoxAnnotationUnits'],
8236            '0x0004': ['CS', '1', 'AnchorPointAnnotationUnits'],
8237            '0x0005': ['CS', '1', 'GraphicAnnotationUnits'],
8238            '0x0006': ['ST', '1', 'UnformattedTextValue'],
8239            '0x0008': ['SQ', '1', 'TextObjectSequence'],
8240            '0x0009': ['SQ', '1', 'GraphicObjectSequence'],
8241            '0x0010': ['FL', '2', 'BoundingBoxTopLeftHandCorner'],
8242            '0x0011': ['FL', '2', 'BoundingBoxBottomRightHandCorner'],
8243            '0x0012': ['CS', '1', 'BoundingBoxTextHorizontalJustification'],
8244            '0x0014': ['FL', '2', 'AnchorPoint'],
8245            '0x0015': ['CS', '1', 'AnchorPointVisibility'],
8246            '0x0020': ['US', '1', 'GraphicDimensions'],
8247            '0x0021': ['US', '1', 'NumberOfGraphicPoints'],
8248            '0x0022': ['FL', '2-n', 'GraphicData'],
8249            '0x0023': ['CS', '1', 'GraphicType'],
8250            '0x0024': ['CS', '1', 'GraphicFilled'],
8251            '0x0040': ['IS', '1', 'ImageRotationRetired'],
8252            '0x0041': ['CS', '1', 'ImageHorizontalFlip'],
8253            '0x0042': ['US', '1', 'ImageRotation'],
8254            '0x0050': ['US', '2', 'DisplayedAreaTopLeftHandCornerTrial'],
8255            '0x0051': ['US', '2', 'DisplayedAreaBottomRightHandCornerTrial'],
8256            '0x0052': ['SL', '2', 'DisplayedAreaTopLeftHandCorner'],
8257            '0x0053': ['SL', '2', 'DisplayedAreaBottomRightHandCorner'],
8258            '0x005A': ['SQ', '1', 'DisplayedAreaSelectionSequence'],
8259            '0x0060': ['SQ', '1', 'GraphicLayerSequence'],
8260            '0x0062': ['IS', '1', 'GraphicLayerOrder'],
8261            '0x0066': ['US', '1', 'GraphicLayerRecommendedDisplayGrayscaleValue'],
8262            '0x0067': ['US', '3', 'GraphicLayerRecommendedDisplayRGBValue'],
8263            '0x0068': ['LO', '1', 'GraphicLayerDescription'],
8264            '0x0080': ['CS', '1', 'ContentLabel'],
8265            '0x0081': ['LO', '1', 'ContentDescription'],
8266            '0x0082': ['DA', '1', 'PresentationCreationDate'],
8267            '0x0083': ['TM', '1', 'PresentationCreationTime'],
8268            '0x0084': ['PN', '1', 'ContentCreatorName'],
8269            '0x0086': ['SQ', '1', 'ContentCreatorIdentificationCodeSequence'],
8270            '0x0087': ['SQ', '1', 'AlternateContentDescriptionSequence'],
8271            '0x0100': ['CS', '1', 'PresentationSizeMode'],
8272            '0x0101': ['DS', '2', 'PresentationPixelSpacing'],
8273            '0x0102': ['IS', '2', 'PresentationPixelAspectRatio'],
8274            '0x0103': ['FL', '1', 'PresentationPixelMagnificationRatio'],
8275            '0x0207': ['LO', '1', 'GraphicGroupLabel'],
8276            '0x0208': ['ST', '1', 'GraphicGroupDescription'],
8277            '0x0209': ['SQ', '1', 'CompoundGraphicSequence'],
8278            '0x0226': ['UL', '1', 'CompoundGraphicInstanceID'],
8279            '0x0227': ['LO', '1', 'FontName'],
8280            '0x0228': ['CS', '1', 'FontNameType'],
8281            '0x0229': ['LO', '1', 'CSSFontName'],
8282            '0x0230': ['FD', '1', 'RotationAngle'],
8283            '0x0231': ['SQ', '1', 'TextStyleSequence'],
8284            '0x0232': ['SQ', '1', 'LineStyleSequence'],
8285            '0x0233': ['SQ', '1', 'FillStyleSequence'],
8286            '0x0234': ['SQ', '1', 'GraphicGroupSequence'],
8287            '0x0241': ['US', '3', 'TextColorCIELabValue'],
8288            '0x0242': ['CS', '1', 'HorizontalAlignment'],
8289            '0x0243': ['CS', '1', 'VerticalAlignment'],
8290            '0x0244': ['CS', '1', 'ShadowStyle'],
8291            '0x0245': ['FL', '1', 'ShadowOffsetX'],
8292            '0x0246': ['FL', '1', 'ShadowOffsetY'],
8293            '0x0247': ['US', '3', 'ShadowColorCIELabValue'],
8294            '0x0248': ['CS', '1', 'Underlined'],
8295            '0x0249': ['CS', '1', 'Bold'],
8296            '0x0250': ['CS', '1', 'Italic'],
8297            '0x0251': ['US', '3', 'PatternOnColorCIELabValue'],
8298            '0x0252': ['US', '3', 'PatternOffColorCIELabValue'],
8299            '0x0253': ['FL', '1', 'LineThickness'],
8300            '0x0254': ['CS', '1', 'LineDashingStyle'],
8301            '0x0255': ['UL', '1', 'LinePattern'],
8302            '0x0256': ['OB', '1', 'FillPattern'],
8303            '0x0257': ['CS', '1', 'FillMode'],
8304            '0x0258': ['FL', '1', 'ShadowOpacity'],
8305            '0x0261': ['FL', '1', 'GapLength'],
8306            '0x0262': ['FL', '1', 'DiameterOfVisibility'],
8307            '0x0273': ['FL', '2', 'RotationPoint'],
8308            '0x0274': ['CS', '1', 'TickAlignment'],
8309            '0x0278': ['CS', '1', 'ShowTickLabel'],
8310            '0x0279': ['CS', '1', 'TickLabelAlignment'],
8311            '0x0282': ['CS', '1', 'CompoundGraphicUnits'],
8312            '0x0284': ['FL', '1', 'PatternOnOpacity'],
8313            '0x0285': ['FL', '1', 'PatternOffOpacity'],
8314            '0x0287': ['SQ', '1', 'MajorTicksSequence'],
8315            '0x0288': ['FL', '1', 'TickPosition'],
8316            '0x0289': ['SH', '1', 'TickLabel'],
8317            '0x0294': ['CS', '1', 'CompoundGraphicType'],
8318            '0x0295': ['UL', '1', 'GraphicGroupID'],
8319            '0x0306': ['CS', '1', 'ShapeType'],
8320            '0x0308': ['SQ', '1', 'RegistrationSequence'],
8321            '0x0309': ['SQ', '1', 'MatrixRegistrationSequence'],
8322            '0x030A': ['SQ', '1', 'MatrixSequence'],
8323            '0x030C': ['CS', '1', 'FrameOfReferenceTransformationMatrixType'],
8324            '0x030D': ['SQ', '1', 'RegistrationTypeCodeSequence'],
8325            '0x030F': ['ST', '1', 'FiducialDescription'],
8326            '0x0310': ['SH', '1', 'FiducialIdentifier'],
8327            '0x0311': ['SQ', '1', 'FiducialIdentifierCodeSequence'],
8328            '0x0312': ['FD', '1', 'ContourUncertaintyRadius'],
8329            '0x0314': ['SQ', '1', 'UsedFiducialsSequence'],
8330            '0x0318': ['SQ', '1', 'GraphicCoordinatesDataSequence'],
8331            '0x031A': ['UI', '1', 'FiducialUID'],
8332            '0x031C': ['SQ', '1', 'FiducialSetSequence'],
8333            '0x031E': ['SQ', '1', 'FiducialSequence'],
8334            '0x0401': ['US', '3', 'GraphicLayerRecommendedDisplayCIELabValue'],
8335            '0x0402': ['SQ', '1', 'BlendingSequence'],
8336            '0x0403': ['FL', '1', 'RelativeOpacity'],
8337            '0x0404': ['SQ', '1', 'ReferencedSpatialRegistrationSequence'],
8338            '0x0405': ['CS', '1', 'BlendingPosition']
8339        },
8340        '0x0072': {
8341            '0x0000': ['UL', '1', 'GenericGroupLength'],
8342            '0x0002': ['SH', '1', 'HangingProtocolName'],
8343            '0x0004': ['LO', '1', 'HangingProtocolDescription'],
8344            '0x0006': ['CS', '1', 'HangingProtocolLevel'],
8345            '0x0008': ['LO', '1', 'HangingProtocolCreator'],
8346            '0x000A': ['DT', '1', 'HangingProtocolCreationDateTime'],
8347            '0x000C': ['SQ', '1', 'HangingProtocolDefinitionSequence'],
8348            '0x000E': ['SQ', '1', 'HangingProtocolUserIdentificationCodeSequence'],
8349            '0x0010': ['LO', '1', 'HangingProtocolUserGroupName'],
8350            '0x0012': ['SQ', '1', 'SourceHangingProtocolSequence'],
8351            '0x0014': ['US', '1', 'NumberOfPriorsReferenced'],
8352            '0x0020': ['SQ', '1', 'ImageSetsSequence'],
8353            '0x0022': ['SQ', '1', 'ImageSetSelectorSequence'],
8354            '0x0024': ['CS', '1', 'ImageSetSelectorUsageFlag'],
8355            '0x0026': ['AT', '1', 'SelectorAttribute'],
8356            '0x0028': ['US', '1', 'SelectorValueNumber'],
8357            '0x0030': ['SQ', '1', 'TimeBasedImageSetsSequence'],
8358            '0x0032': ['US', '1', 'ImageSetNumber'],
8359            '0x0034': ['CS', '1', 'ImageSetSelectorCategory'],
8360            '0x0038': ['US', '2', 'RelativeTime'],
8361            '0x003A': ['CS', '1', 'RelativeTimeUnits'],
8362            '0x003C': ['SS', '2', 'AbstractPriorValue'],
8363            '0x003E': ['SQ', '1', 'AbstractPriorCodeSequence'],
8364            '0x0040': ['LO', '1', 'ImageSetLabel'],
8365            '0x0050': ['CS', '1', 'SelectorAttributeVR'],
8366            '0x0052': ['AT', '1-n', 'SelectorSequencePointer'],
8367            '0x0054': ['LO', '1-n', 'SelectorSequencePointerPrivateCreator'],
8368            '0x0056': ['LO', '1', 'SelectorAttributePrivateCreator'],
8369            '0x0060': ['AT', '1-n', 'SelectorATValue'],
8370            '0x0062': ['CS', '1-n', 'SelectorCSValue'],
8371            '0x0064': ['IS', '1-n', 'SelectorISValue'],
8372            '0x0066': ['LO', '1-n', 'SelectorLOValue'],
8373            '0x0068': ['LT', '1', 'SelectorLTValue'],
8374            '0x006A': ['PN', '1-n', 'SelectorPNValue'],
8375            '0x006C': ['SH', '1-n', 'SelectorSHValue'],
8376            '0x006E': ['ST', '1', 'SelectorSTValue'],
8377            '0x0070': ['UT', '1', 'SelectorUTValue'],
8378            '0x0072': ['DS', '1-n', 'SelectorDSValue'],
8379            '0x0074': ['FD', '1-n', 'SelectorFDValue'],
8380            '0x0076': ['FL', '1-n', 'SelectorFLValue'],
8381            '0x0078': ['UL', '1-n', 'SelectorULValue'],
8382            '0x007A': ['US', '1-n', 'SelectorUSValue'],
8383            '0x007C': ['SL', '1-n', 'SelectorSLValue'],
8384            '0x007E': ['SS', '1-n', 'SelectorSSValue'],
8385            '0x007F': ['UI', '1-n', 'SelectorUIValue'],
8386            '0x0080': ['SQ', '1', 'SelectorCodeSequenceValue'],
8387            '0x0100': ['US', '1', 'NumberOfScreens'],
8388            '0x0102': ['SQ', '1', 'NominalScreenDefinitionSequence'],
8389            '0x0104': ['US', '1', 'NumberOfVerticalPixels'],
8390            '0x0106': ['US', '1', 'NumberOfHorizontalPixels'],
8391            '0x0108': ['FD', '4', 'DisplayEnvironmentSpatialPosition'],
8392            '0x010A': ['US', '1', 'ScreenMinimumGrayscaleBitDepth'],
8393            '0x010C': ['US', '1', 'ScreenMinimumColorBitDepth'],
8394            '0x010E': ['US', '1', 'ApplicationMaximumRepaintTime'],
8395            '0x0200': ['SQ', '1', 'DisplaySetsSequence'],
8396            '0x0202': ['US', '1', 'DisplaySetNumber'],
8397            '0x0203': ['LO', '1', 'DisplaySetLabel'],
8398            '0x0204': ['US', '1', 'DisplaySetPresentationGroup'],
8399            '0x0206': ['LO', '1', 'DisplaySetPresentationGroupDescription'],
8400            '0x0208': ['CS', '1', 'PartialDataDisplayHandling'],
8401            '0x0210': ['SQ', '1', 'SynchronizedScrollingSequence'],
8402            '0x0212': ['US', '2-n', 'DisplaySetScrollingGroup'],
8403            '0x0214': ['SQ', '1', 'NavigationIndicatorSequence'],
8404            '0x0216': ['US', '1', 'NavigationDisplaySet'],
8405            '0x0218': ['US', '1-n', 'ReferenceDisplaySets'],
8406            '0x0300': ['SQ', '1', 'ImageBoxesSequence'],
8407            '0x0302': ['US', '1', 'ImageBoxNumber'],
8408            '0x0304': ['CS', '1', 'ImageBoxLayoutType'],
8409            '0x0306': ['US', '1', 'ImageBoxTileHorizontalDimension'],
8410            '0x0308': ['US', '1', 'ImageBoxTileVerticalDimension'],
8411            '0x0310': ['CS', '1', 'ImageBoxScrollDirection'],
8412            '0x0312': ['CS', '1', 'ImageBoxSmallScrollType'],
8413            '0x0314': ['US', '1', 'ImageBoxSmallScrollAmount'],
8414            '0x0316': ['CS', '1', 'ImageBoxLargeScrollType'],
8415            '0x0318': ['US', '1', 'ImageBoxLargeScrollAmount'],
8416            '0x0320': ['US', '1', 'ImageBoxOverlapPriority'],
8417            '0x0330': ['FD', '1', 'CineRelativeToRealTime'],
8418            '0x0400': ['SQ', '1', 'FilterOperationsSequence'],
8419            '0x0402': ['CS', '1', 'FilterByCategory'],
8420            '0x0404': ['CS', '1', 'FilterByAttributePresence'],
8421            '0x0406': ['CS', '1', 'FilterByOperator'],
8422            '0x0420': ['US', '3', 'StructuredDisplayBackgroundCIELabValue'],
8423            '0x0421': ['US', '3', 'EmptyImageBoxCIELabValue'],
8424            '0x0422': ['SQ', '1', 'StructuredDisplayImageBoxSequence'],
8425            '0x0424': ['SQ', '1', 'StructuredDisplayTextBoxSequence'],
8426            '0x0427': ['SQ', '1', 'ReferencedFirstFrameSequence'],
8427            '0x0430': ['SQ', '1', 'ImageBoxSynchronizationSequence'],
8428            '0x0432': ['US', '2-n', 'SynchronizedImageBoxList'],
8429            '0x0434': ['CS', '1', 'TypeOfSynchronization'],
8430            '0x0500': ['CS', '1', 'BlendingOperationType'],
8431            '0x0510': ['CS', '1', 'ReformattingOperationType'],
8432            '0x0512': ['FD', '1', 'ReformattingThickness'],
8433            '0x0514': ['FD', '1', 'ReformattingInterval'],
8434            '0x0516': ['CS', '1', 'ReformattingOperationInitialViewDirection'],
8435            '0x0520': ['CS', '1-n', 'ThreeDRenderingType'],
8436            '0x0600': ['SQ', '1', 'SortingOperationsSequence'],
8437            '0x0602': ['CS', '1', 'SortByCategory'],
8438            '0x0604': ['CS', '1', 'SortingDirection'],
8439            '0x0700': ['CS', '2', 'DisplaySetPatientOrientation'],
8440            '0x0702': ['CS', '1', 'VOIType'],
8441            '0x0704': ['CS', '1', 'PseudoColorType'],
8442            '0x0705': ['SQ', '1', 'PseudoColorPaletteInstanceReferenceSequence'],
8443            '0x0706': ['CS', '1', 'ShowGrayscaleInverted'],
8444            '0x0710': ['CS', '1', 'ShowImageTrueSizeFlag'],
8445            '0x0712': ['CS', '1', 'ShowGraphicAnnotationFlag'],
8446            '0x0714': ['CS', '1', 'ShowPatientDemographicsFlag'],
8447            '0x0716': ['CS', '1', 'ShowAcquisitionTechniquesFlag'],
8448            '0x0717': ['CS', '1', 'DisplaySetHorizontalJustification'],
8449            '0x0718': ['CS', '1', 'DisplaySetVerticalJustification']
8450        },
8451        '0x0074': {
8452            '0x0000': ['UL', '1', 'GenericGroupLength'],
8453            '0x0120': ['FD', '1', 'ContinuationStartMeterset'],
8454            '0x0121': ['FD', '1', 'ContinuationEndMeterset'],
8455            '0x1000': ['CS', '1', 'ProcedureStepState'],
8456            '0x1002': ['SQ', '1', 'ProcedureStepProgressInformationSequence'],
8457            '0x1004': ['DS', '1', 'ProcedureStepProgress'],
8458            '0x1006': ['ST', '1', 'ProcedureStepProgressDescription'],
8459            '0x1008': ['SQ', '1', 'ProcedureStepCommunicationsURISequence'],
8460            '0x100A': ['UR', '1', 'ContactURI'],
8461            '0x100C': ['LO', '1', 'ContactDisplayName'],
8462            '0x100E': ['SQ', '1', 'ProcedureStepDiscontinuationReasonCodeSequence'],
8463            '0x1020': ['SQ', '1', 'BeamTaskSequence'],
8464            '0x1022': ['CS', '1', 'BeamTaskType'],
8465            '0x1024': ['IS', '1', 'BeamOrderIndexTrial'],
8466            '0x1025': ['CS', '1', 'AutosequenceFlag'],
8467            '0x1026': ['FD', '1', 'TableTopVerticalAdjustedPosition'],
8468            '0x1027': ['FD', '1', 'TableTopLongitudinalAdjustedPosition'],
8469            '0x1028': ['FD', '1', 'TableTopLateralAdjustedPosition'],
8470            '0x102A': ['FD', '1', 'PatientSupportAdjustedAngle'],
8471            '0x102B': ['FD', '1', 'TableTopEccentricAdjustedAngle'],
8472            '0x102C': ['FD', '1', 'TableTopPitchAdjustedAngle'],
8473            '0x102D': ['FD', '1', 'TableTopRollAdjustedAngle'],
8474            '0x1030': ['SQ', '1', 'DeliveryVerificationImageSequence'],
8475            '0x1032': ['CS', '1', 'VerificationImageTiming'],
8476            '0x1034': ['CS', '1', 'DoubleExposureFlag'],
8477            '0x1036': ['CS', '1', 'DoubleExposureOrdering'],
8478            '0x1038': ['DS', '1', 'DoubleExposureMetersetTrial'],
8479            '0x103A': ['DS', '4', 'DoubleExposureFieldDeltaTrial'],
8480            '0x1040': ['SQ', '1', 'RelatedReferenceRTImageSequence'],
8481            '0x1042': ['SQ', '1', 'GeneralMachineVerificationSequence'],
8482            '0x1044': ['SQ', '1', 'ConventionalMachineVerificationSequence'],
8483            '0x1046': ['SQ', '1', 'IonMachineVerificationSequence'],
8484            '0x1048': ['SQ', '1', 'FailedAttributesSequence'],
8485            '0x104A': ['SQ', '1', 'OverriddenAttributesSequence'],
8486            '0x104C': ['SQ', '1', 'ConventionalControlPointVerificationSequence'],
8487            '0x104E': ['SQ', '1', 'IonControlPointVerificationSequence'],
8488            '0x1050': ['SQ', '1', 'AttributeOccurrenceSequence'],
8489            '0x1052': ['AT', '1', 'AttributeOccurrencePointer'],
8490            '0x1054': ['UL', '1', 'AttributeItemSelector'],
8491            '0x1056': ['LO', '1', 'AttributeOccurrencePrivateCreator'],
8492            '0x1057': ['IS', '1-n', 'SelectorSequencePointerItems'],
8493            '0x1200': ['CS', '1', 'ScheduledProcedureStepPriority'],
8494            '0x1202': ['LO', '1', 'WorklistLabel'],
8495            '0x1204': ['LO', '1', 'ProcedureStepLabel'],
8496            '0x1210': ['SQ', '1', 'ScheduledProcessingParametersSequence'],
8497            '0x1212': ['SQ', '1', 'PerformedProcessingParametersSequence'],
8498            '0x1216': ['SQ', '1', 'UnifiedProcedureStepPerformedProcedureSequence'],
8499            '0x1220': ['SQ', '1', 'RelatedProcedureStepSequence'],
8500            '0x1222': ['LO', '1', 'ProcedureStepRelationshipType'],
8501            '0x1224': ['SQ', '1', 'ReplacedProcedureStepSequence'],
8502            '0x1230': ['LO', '1', 'DeletionLock'],
8503            '0x1234': ['AE', '1', 'ReceivingAE'],
8504            '0x1236': ['AE', '1', 'RequestingAE'],
8505            '0x1238': ['LT', '1', 'ReasonForCancellation'],
8506            '0x1242': ['CS', '1', 'SCPStatus'],
8507            '0x1244': ['CS', '1', 'SubscriptionListStatus'],
8508            '0x1246': ['CS', '1', 'UnifiedProcedureStepListStatus'],
8509            '0x1324': ['UL', '1', 'BeamOrderIndex'],
8510            '0x1338': ['FD', '1', 'DoubleExposureMeterset'],
8511            '0x133A': ['FD', '4', 'DoubleExposureFieldDelta']
8512        },
8513        '0x0076': {
8514            '0x0000': ['UL', '1', 'GenericGroupLength'],
8515            '0x0001': ['LO', '1', 'ImplantAssemblyTemplateName'],
8516            '0x0003': ['LO', '1', 'ImplantAssemblyTemplateIssuer'],
8517            '0x0006': ['LO', '1', 'ImplantAssemblyTemplateVersion'],
8518            '0x0008': ['SQ', '1', 'ReplacedImplantAssemblyTemplateSequence'],
8519            '0x000A': ['CS', '1', 'ImplantAssemblyTemplateType'],
8520            '0x000C': ['SQ', '1', 'OriginalImplantAssemblyTemplateSequence'],
8521            '0x000E': ['SQ', '1', 'DerivationImplantAssemblyTemplateSequence'],
8522            '0x0010': ['SQ', '1', 'ImplantAssemblyTemplateTargetAnatomySequence'],
8523            '0x0020': ['SQ', '1', 'ProcedureTypeCodeSequence'],
8524            '0x0030': ['LO', '1', 'SurgicalTechnique'],
8525            '0x0032': ['SQ', '1', 'ComponentTypesSequence'],
8526            '0x0034': ['CS', '1', 'ComponentTypeCodeSequence'],
8527            '0x0036': ['CS', '1', 'ExclusiveComponentType'],
8528            '0x0038': ['CS', '1', 'MandatoryComponentType'],
8529            '0x0040': ['SQ', '1', 'ComponentSequence'],
8530            '0x0055': ['US', '1', 'ComponentID'],
8531            '0x0060': ['SQ', '1', 'ComponentAssemblySequence'],
8532            '0x0070': ['US', '1', 'Component1ReferencedID'],
8533            '0x0080': ['US', '1', 'Component1ReferencedMatingFeatureSetID'],
8534            '0x0090': ['US', '1', 'Component1ReferencedMatingFeatureID'],
8535            '0x00A0': ['US', '1', 'Component2ReferencedID'],
8536            '0x00B0': ['US', '1', 'Component2ReferencedMatingFeatureSetID'],
8537            '0x00C0': ['US', '1', 'Component2ReferencedMatingFeatureID']
8538        },
8539        '0x0078': {
8540            '0x0000': ['UL', '1', 'GenericGroupLength'],
8541            '0x0001': ['LO', '1', 'ImplantTemplateGroupName'],
8542            '0x0010': ['ST', '1', 'ImplantTemplateGroupDescription'],
8543            '0x0020': ['LO', '1', 'ImplantTemplateGroupIssuer'],
8544            '0x0024': ['LO', '1', 'ImplantTemplateGroupVersion'],
8545            '0x0026': ['SQ', '1', 'ReplacedImplantTemplateGroupSequence'],
8546            '0x0028': ['SQ', '1', 'ImplantTemplateGroupTargetAnatomySequence'],
8547            '0x002A': ['SQ', '1', 'ImplantTemplateGroupMembersSequence'],
8548            '0x002E': ['US', '1', 'ImplantTemplateGroupMemberID'],
8549            '0x0050': ['FD', '3', 'ThreeDImplantTemplateGroupMemberMatchingPoint'],
8550            '0x0060': ['FD', '9', 'ThreeDImplantTemplateGroupMemberMatchingAxes'],
8551            '0x0070': ['SQ', '1', 'ImplantTemplateGroupMemberMatching2DCoordinatesSequence'],
8552            '0x0090': ['FD', '2', 'TwoDImplantTemplateGroupMemberMatchingPoint'],
8553            '0x00A0': ['FD', '4', 'TwoDImplantTemplateGroupMemberMatchingAxes'],
8554            '0x00B0': ['SQ', '1', 'ImplantTemplateGroupVariationDimensionSequence'],
8555            '0x00B2': ['LO', '1', 'ImplantTemplateGroupVariationDimensionName'],
8556            '0x00B4': ['SQ', '1', 'ImplantTemplateGroupVariationDimensionRankSequence'],
8557            '0x00B6': ['US', '1', 'ReferencedImplantTemplateGroupMemberID'],
8558            '0x00B8': ['US', '1', 'ImplantTemplateGroupVariationDimensionRank']
8559        },
8560        '0x0080': {
8561            '0x0000': ['UL', '1', 'GenericGroupLength'],
8562            '0x0001': ['SQ', '1', 'SurfaceScanAcquisitionTypeCodeSequence'],
8563            '0x0002': ['SQ', '1', 'SurfaceScanModeCodeSequence'],
8564            '0x0003': ['SQ', '1', 'RegistrationMethodCodeSequence'],
8565            '0x0004': ['FD', '1', 'ShotDurationTime'],
8566            '0x0005': ['FD', '1', 'ShotOffsetTime'],
8567            '0x0006': ['US', '1-n', 'SurfacePointPresentationValueData'],
8568            '0x0007': ['US', '3-3n', 'SurfacePointColorCIELabValueData'],
8569            '0x0008': ['SQ', '1', 'UVMappingSequence'],
8570            '0x0009': ['SH', '1', 'TextureLabel'],
8571            '0x0010': ['OF', '1-n', 'UValueData'],
8572            '0x0011': ['OF', '1-n', 'VValueData'],
8573            '0x0012': ['SQ', '1', 'ReferencedTextureSequence'],
8574            '0x0013': ['SQ', '1', 'ReferencedSurfaceDataSequence']
8575        },
8576        '0x0088': {
8577            '0x0000': ['UL', '1', 'GenericGroupLength'],
8578            '0x0130': ['SH', '1', 'StorageMediaFileSetID'],
8579            '0x0140': ['UI', '1', 'StorageMediaFileSetUID'],
8580            '0x0200': ['SQ', '1', 'IconImageSequence'],
8581            '0x0904': ['LO', '1', 'TopicTitle'],
8582            '0x0906': ['ST', '1', 'TopicSubject'],
8583            '0x0910': ['LO', '1', 'TopicAuthor'],
8584            '0x0912': ['LO', '1-32', 'TopicKeywords']
8585        },
8586        '0x0100': {
8587            '0x0000': ['UL', '1', 'GenericGroupLength'],
8588            '0x0410': ['CS', '1', 'SOPInstanceStatus'],
8589            '0x0420': ['DT', '1', 'SOPAuthorizationDateTime'],
8590            '0x0424': ['LT', '1', 'SOPAuthorizationComment'],
8591            '0x0426': ['LO', '1', 'AuthorizationEquipmentCertificationNumber']
8592        },
8593        '0x0400': {
8594            '0x0000': ['UL', '1', 'GenericGroupLength'],
8595            '0x0005': ['US', '1', 'MACIDNumber'],
8596            '0x0010': ['UI', '1', 'MACCalculationTransferSyntaxUID'],
8597            '0x0015': ['CS', '1', 'MACAlgorithm'],
8598            '0x0020': ['AT', '1-n', 'DataElementsSigned'],
8599            '0x0100': ['UI', '1', 'DigitalSignatureUID'],
8600            '0x0105': ['DT', '1', 'DigitalSignatureDateTime'],
8601            '0x0110': ['CS', '1', 'CertificateType'],
8602            '0x0115': ['OB', '1', 'CertificateOfSigner'],
8603            '0x0120': ['OB', '1', 'Signature'],
8604            '0x0305': ['CS', '1', 'CertifiedTimestampType'],
8605            '0x0310': ['OB', '1', 'CertifiedTimestamp'],
8606            '0x0401': ['SQ', '1', 'DigitalSignaturePurposeCodeSequence'],
8607            '0x0402': ['SQ', '1', 'ReferencedDigitalSignatureSequence'],
8608            '0x0403': ['SQ', '1', 'ReferencedSOPInstanceMACSequence'],
8609            '0x0404': ['OB', '1', 'MAC'],
8610            '0x0500': ['SQ', '1', 'EncryptedAttributesSequence'],
8611            '0x0510': ['UI', '1', 'EncryptedContentTransferSyntaxUID'],
8612            '0x0520': ['OB', '1', 'EncryptedContent'],
8613            '0x0550': ['SQ', '1', 'ModifiedAttributesSequence'],
8614            '0x0561': ['SQ', '1', 'OriginalAttributesSequence'],
8615            '0x0562': ['DT', '1', 'AttributeModificationDateTime'],
8616            '0x0563': ['LO', '1', 'ModifyingSystem'],
8617            '0x0564': ['LO', '1', 'SourceOfPreviousValues'],
8618            '0x0565': ['CS', '1', 'ReasonForTheAttributeModification']
8619        },
8620        '0x1000': {
8621            '0x0000': ['UL', '1', 'GenericGroupLength'],
8622            '0x0010': ['US', '3', 'EscapeTriplet'],
8623            '0x0011': ['US', '3', 'RunLengthTriplet'],
8624            '0x0012': ['US', '1', 'HuffmanTableSize'],
8625            '0x0013': ['US', '3', 'HuffmanTableTriplet'],
8626            '0x0014': ['US', '1', 'ShiftTableSize'],
8627            '0x0015': ['US', '3', 'ShiftTableTriplet']
8628        },
8629        '0x1010': {
8630            '0x0000': ['UL', '1', 'GenericGroupLength'],
8631            '0x0004': ['US', '1-n', 'ZonalMap']
8632        },
8633        '0x2000': {
8634            '0x0000': ['UL', '1', 'GenericGroupLength'],
8635            '0x0010': ['IS', '1', 'NumberOfCopies'],
8636            '0x001E': ['SQ', '1', 'PrinterConfigurationSequence'],
8637            '0x0020': ['CS', '1', 'PrintPriority'],
8638            '0x0030': ['CS', '1', 'MediumType'],
8639            '0x0040': ['CS', '1', 'FilmDestination'],
8640            '0x0050': ['LO', '1', 'FilmSessionLabel'],
8641            '0x0060': ['IS', '1', 'MemoryAllocation'],
8642            '0x0061': ['IS', '1', 'MaximumMemoryAllocation'],
8643            '0x0062': ['CS', '1', 'ColorImagePrintingFlag'],
8644            '0x0063': ['CS', '1', 'CollationFlag'],
8645            '0x0065': ['CS', '1', 'AnnotationFlag'],
8646            '0x0067': ['CS', '1', 'ImageOverlayFlag'],
8647            '0x0069': ['CS', '1', 'PresentationLUTFlag'],
8648            '0x006A': ['CS', '1', 'ImageBoxPresentationLUTFlag'],
8649            '0x00A0': ['US', '1', 'MemoryBitDepth'],
8650            '0x00A1': ['US', '1', 'PrintingBitDepth'],
8651            '0x00A2': ['SQ', '1', 'MediaInstalledSequence'],
8652            '0x00A4': ['SQ', '1', 'OtherMediaAvailableSequence'],
8653            '0x00A8': ['SQ', '1', 'SupportedImageDisplayFormatsSequence'],
8654            '0x0500': ['SQ', '1', 'ReferencedFilmBoxSequence'],
8655            '0x0510': ['SQ', '1', 'ReferencedStoredPrintSequence']
8656        },
8657        '0x2010': {
8658            '0x0000': ['UL', '1', 'GenericGroupLength'],
8659            '0x0010': ['ST', '1', 'ImageDisplayFormat'],
8660            '0x0030': ['CS', '1', 'AnnotationDisplayFormatID'],
8661            '0x0040': ['CS', '1', 'FilmOrientation'],
8662            '0x0050': ['CS', '1', 'FilmSizeID'],
8663            '0x0052': ['CS', '1', 'PrinterResolutionID'],
8664            '0x0054': ['CS', '1', 'DefaultPrinterResolutionID'],
8665            '0x0060': ['CS', '1', 'MagnificationType'],
8666            '0x0080': ['CS', '1', 'SmoothingType'],
8667            '0x00A6': ['CS', '1', 'DefaultMagnificationType'],
8668            '0x00A7': ['CS', '1-n', 'OtherMagnificationTypesAvailable'],
8669            '0x00A8': ['CS', '1', 'DefaultSmoothingType'],
8670            '0x00A9': ['CS', '1-n', 'OtherSmoothingTypesAvailable'],
8671            '0x0100': ['CS', '1', 'BorderDensity'],
8672            '0x0110': ['CS', '1', 'EmptyImageDensity'],
8673            '0x0120': ['US', '1', 'MinDensity'],
8674            '0x0130': ['US', '1', 'MaxDensity'],
8675            '0x0140': ['CS', '1', 'Trim'],
8676            '0x0150': ['ST', '1', 'ConfigurationInformation'],
8677            '0x0152': ['LT', '1', 'ConfigurationInformationDescription'],
8678            '0x0154': ['IS', '1', 'MaximumCollatedFilms'],
8679            '0x015E': ['US', '1', 'Illumination'],
8680            '0x0160': ['US', '1', 'ReflectedAmbientLight'],
8681            '0x0376': ['DS', '2', 'PrinterPixelSpacing'],
8682            '0x0500': ['SQ', '1', 'ReferencedFilmSessionSequence'],
8683            '0x0510': ['SQ', '1', 'ReferencedImageBoxSequence'],
8684            '0x0520': ['SQ', '1', 'ReferencedBasicAnnotationBoxSequence']
8685        },
8686        '0x2020': {
8687            '0x0000': ['UL', '1', 'GenericGroupLength'],
8688            '0x0010': ['US', '1', 'ImageBoxPosition'],
8689            '0x0020': ['CS', '1', 'Polarity'],
8690            '0x0030': ['DS', '1', 'RequestedImageSize'],
8691            '0x0040': ['CS', '1', 'RequestedDecimateCropBehavior'],
8692            '0x0050': ['CS', '1', 'RequestedResolutionID'],
8693            '0x00A0': ['CS', '1', 'RequestedImageSizeFlag'],
8694            '0x00A2': ['CS', '1', 'DecimateCropResult'],
8695            '0x0110': ['SQ', '1', 'BasicGrayscaleImageSequence'],
8696            '0x0111': ['SQ', '1', 'BasicColorImageSequence'],
8697            '0x0130': ['SQ', '1', 'ReferencedImageOverlayBoxSequence'],
8698            '0x0140': ['SQ', '1', 'ReferencedVOILUTBoxSequence']
8699        },
8700        '0x2030': {
8701            '0x0000': ['UL', '1', 'GenericGroupLength'],
8702            '0x0010': ['US', '1', 'AnnotationPosition'],
8703            '0x0020': ['LO', '1', 'TextString']
8704        },
8705        '0x2040': {
8706            '0x0000': ['UL', '1', 'GenericGroupLength'],
8707            '0x0010': ['SQ', '1', 'ReferencedOverlayPlaneSequence'],
8708            '0x0011': ['US', '1-99', 'ReferencedOverlayPlaneGroups'],
8709            '0x0020': ['SQ', '1', 'OverlayPixelDataSequence'],
8710            '0x0060': ['CS', '1', 'OverlayMagnificationType'],
8711            '0x0070': ['CS', '1', 'OverlaySmoothingType'],
8712            '0x0072': ['CS', '1', 'OverlayOrImageMagnification'],
8713            '0x0074': ['US', '1', 'MagnifyToNumberOfColumns'],
8714            '0x0080': ['CS', '1', 'OverlayForegroundDensity'],
8715            '0x0082': ['CS', '1', 'OverlayBackgroundDensity'],
8716            '0x0090': ['CS', '1', 'OverlayMode'],
8717            '0x0100': ['CS', '1', 'ThresholdDensity'],
8718            '0x0500': ['SQ', '1', 'ReferencedImageBoxSequenceRetired']
8719        },
8720        '0x2050': {
8721            '0x0000': ['UL', '1', 'GenericGroupLength'],
8722            '0x0010': ['SQ', '1', 'PresentationLUTSequence'],
8723            '0x0020': ['CS', '1', 'PresentationLUTShape'],
8724            '0x0500': ['SQ', '1', 'ReferencedPresentationLUTSequence']
8725        },
8726        '0x2100': {
8727            '0x0000': ['UL', '1', 'GenericGroupLength'],
8728            '0x0010': ['SH', '1', 'PrintJobID'],
8729            '0x0020': ['CS', '1', 'ExecutionStatus'],
8730            '0x0030': ['CS', '1', 'ExecutionStatusInfo'],
8731            '0x0040': ['DA', '1', 'CreationDate'],
8732            '0x0050': ['TM', '1', 'CreationTime'],
8733            '0x0070': ['AE', '1', 'Originator'],
8734            '0x0140': ['AE', '1', 'DestinationAE'],
8735            '0x0160': ['SH', '1', 'OwnerID'],
8736            '0x0170': ['IS', '1', 'NumberOfFilms'],
8737            '0x0500': ['SQ', '1', 'ReferencedPrintJobSequencePullStoredPrint']
8738        },
8739        '0x2110': {
8740            '0x0000': ['UL', '1', 'GenericGroupLength'],
8741            '0x0010': ['CS', '1', 'PrinterStatus'],
8742            '0x0020': ['CS', '1', 'PrinterStatusInfo'],
8743            '0x0030': ['LO', '1', 'PrinterName'],
8744            '0x0099': ['SH', '1', 'PrintQueueID']
8745        },
8746        '0x2120': {
8747            '0x0000': ['UL', '1', 'GenericGroupLength'],
8748            '0x0010': ['CS', '1', 'QueueStatus'],
8749            '0x0050': ['SQ', '1', 'PrintJobDescriptionSequence'],
8750            '0x0070': ['SQ', '1', 'ReferencedPrintJobSequence']
8751        },
8752        '0x2130': {
8753            '0x0000': ['UL', '1', 'GenericGroupLength'],
8754            '0x0010': ['SQ', '1', 'PrintManagementCapabilitiesSequence'],
8755            '0x0015': ['SQ', '1', 'PrinterCharacteristicsSequence'],
8756            '0x0030': ['SQ', '1', 'FilmBoxContentSequence'],
8757            '0x0040': ['SQ', '1', 'ImageBoxContentSequence'],
8758            '0x0050': ['SQ', '1', 'AnnotationContentSequence'],
8759            '0x0060': ['SQ', '1', 'ImageOverlayBoxContentSequence'],
8760            '0x0080': ['SQ', '1', 'PresentationLUTContentSequence'],
8761            '0x00A0': ['SQ', '1', 'ProposedStudySequence'],
8762            '0x00C0': ['SQ', '1', 'OriginalImageSequence']
8763        },
8764        '0x2200': {
8765            '0x0000': ['UL', '1', 'GenericGroupLength'],
8766            '0x0001': ['CS', '1', 'LabelUsingInformationExtractedFromInstances'],
8767            '0x0002': ['UT', '1', 'LabelText'],
8768            '0x0003': ['CS', '1', 'LabelStyleSelection'],
8769            '0x0004': ['LT', '1', 'MediaDisposition'],
8770            '0x0005': ['LT', '1', 'BarcodeValue'],
8771            '0x0006': ['CS', '1', 'BarcodeSymbology'],
8772            '0x0007': ['CS', '1', 'AllowMediaSplitting'],
8773            '0x0008': ['CS', '1', 'IncludeNonDICOMObjects'],
8774            '0x0009': ['CS', '1', 'IncludeDisplayApplication'],
8775            '0x000A': ['CS', '1', 'PreserveCompositeInstancesAfterMediaCreation'],
8776            '0x000B': ['US', '1', 'TotalNumberOfPiecesOfMediaCreated'],
8777            '0x000C': ['LO', '1', 'RequestedMediaApplicationProfile'],
8778            '0x000D': ['SQ', '1', 'ReferencedStorageMediaSequence'],
8779            '0x000E': ['AT', '1-n', 'FailureAttributes'],
8780            '0x000F': ['CS', '1', 'AllowLossyCompression'],
8781            '0x0020': ['CS', '1', 'RequestPriority']
8782        },
8783        '0x3002': {
8784            '0x0000': ['UL', '1', 'GenericGroupLength'],
8785            '0x0002': ['SH', '1', 'RTImageLabel'],
8786            '0x0003': ['LO', '1', 'RTImageName'],
8787            '0x0004': ['ST', '1', 'RTImageDescription'],
8788            '0x000A': ['CS', '1', 'ReportedValuesOrigin'],
8789            '0x000C': ['CS', '1', 'RTImagePlane'],
8790            '0x000D': ['DS', '3', 'XRayImageReceptorTranslation'],
8791            '0x000E': ['DS', '1', 'XRayImageReceptorAngle'],
8792            '0x0010': ['DS', '6', 'RTImageOrientation'],
8793            '0x0011': ['DS', '2', 'ImagePlanePixelSpacing'],
8794            '0x0012': ['DS', '2', 'RTImagePosition'],
8795            '0x0020': ['SH', '1', 'RadiationMachineName'],
8796            '0x0022': ['DS', '1', 'RadiationMachineSAD'],
8797            '0x0024': ['DS', '1', 'RadiationMachineSSD'],
8798            '0x0026': ['DS', '1', 'RTImageSID'],
8799            '0x0028': ['DS', '1', 'SourceToReferenceObjectDistance'],
8800            '0x0029': ['IS', '1', 'FractionNumber'],
8801            '0x0030': ['SQ', '1', 'ExposureSequence'],
8802            '0x0032': ['DS', '1', 'MetersetExposure'],
8803            '0x0034': ['DS', '4', 'DiaphragmPosition'],
8804            '0x0040': ['SQ', '1', 'FluenceMapSequence'],
8805            '0x0041': ['CS', '1', 'FluenceDataSource'],
8806            '0x0042': ['DS', '1', 'FluenceDataScale'],
8807            '0x0050': ['SQ', '1', 'PrimaryFluenceModeSequence'],
8808            '0x0051': ['CS', '1', 'FluenceMode'],
8809            '0x0052': ['SH', '1', 'FluenceModeID']
8810        },
8811        '0x3004': {
8812            '0x0000': ['UL', '1', 'GenericGroupLength'],
8813            '0x0001': ['CS', '1', 'DVHType'],
8814            '0x0002': ['CS', '1', 'DoseUnits'],
8815            '0x0004': ['CS', '1', 'DoseType'],
8816            '0x0005': ['CS', '1', 'SpatialTransformOfDose'],
8817            '0x0006': ['LO', '1', 'DoseComment'],
8818            '0x0008': ['DS', '3', 'NormalizationPoint'],
8819            '0x000A': ['CS', '1', 'DoseSummationType'],
8820            '0x000C': ['DS', '2-n', 'GridFrameOffsetVector'],
8821            '0x000E': ['DS', '1', 'DoseGridScaling'],
8822            '0x0010': ['SQ', '1', 'RTDoseROISequence'],
8823            '0x0012': ['DS', '1', 'DoseValue'],
8824            '0x0014': ['CS', '1-3', 'TissueHeterogeneityCorrection'],
8825            '0x0040': ['DS', '3', 'DVHNormalizationPoint'],
8826            '0x0042': ['DS', '1', 'DVHNormalizationDoseValue'],
8827            '0x0050': ['SQ', '1', 'DVHSequence'],
8828            '0x0052': ['DS', '1', 'DVHDoseScaling'],
8829            '0x0054': ['CS', '1', 'DVHVolumeUnits'],
8830            '0x0056': ['IS', '1', 'DVHNumberOfBins'],
8831            '0x0058': ['DS', '2-2n', 'DVHData'],
8832            '0x0060': ['SQ', '1', 'DVHReferencedROISequence'],
8833            '0x0062': ['CS', '1', 'DVHROIContributionType'],
8834            '0x0070': ['DS', '1', 'DVHMinimumDose'],
8835            '0x0072': ['DS', '1', 'DVHMaximumDose'],
8836            '0x0074': ['DS', '1', 'DVHMeanDose']
8837        },
8838        '0x3006': {
8839            '0x0000': ['UL', '1', 'GenericGroupLength'],
8840            '0x0002': ['SH', '1', 'StructureSetLabel'],
8841            '0x0004': ['LO', '1', 'StructureSetName'],
8842            '0x0006': ['ST', '1', 'StructureSetDescription'],
8843            '0x0008': ['DA', '1', 'StructureSetDate'],
8844            '0x0009': ['TM', '1', 'StructureSetTime'],
8845            '0x0010': ['SQ', '1', 'ReferencedFrameOfReferenceSequence'],
8846            '0x0012': ['SQ', '1', 'RTReferencedStudySequence'],
8847            '0x0014': ['SQ', '1', 'RTReferencedSeriesSequence'],
8848            '0x0016': ['SQ', '1', 'ContourImageSequence'],
8849            '0x0018': ['SQ', '1', 'PredecessorStructureSetSequence'],
8850            '0x0020': ['SQ', '1', 'StructureSetROISequence'],
8851            '0x0022': ['IS', '1', 'ROINumber'],
8852            '0x0024': ['UI', '1', 'ReferencedFrameOfReferenceUID'],
8853            '0x0026': ['LO', '1', 'ROIName'],
8854            '0x0028': ['ST', '1', 'ROIDescription'],
8855            '0x002A': ['IS', '3', 'ROIDisplayColor'],
8856            '0x002C': ['DS', '1', 'ROIVolume'],
8857            '0x0030': ['SQ', '1', 'RTRelatedROISequence'],
8858            '0x0033': ['CS', '1', 'RTROIRelationship'],
8859            '0x0036': ['CS', '1', 'ROIGenerationAlgorithm'],
8860            '0x0038': ['LO', '1', 'ROIGenerationDescription'],
8861            '0x0039': ['SQ', '1', 'ROIContourSequence'],
8862            '0x0040': ['SQ', '1', 'ContourSequence'],
8863            '0x0042': ['CS', '1', 'ContourGeometricType'],
8864            '0x0044': ['DS', '1', 'ContourSlabThickness'],
8865            '0x0045': ['DS', '3', 'ContourOffsetVector'],
8866            '0x0046': ['IS', '1', 'NumberOfContourPoints'],
8867            '0x0048': ['IS', '1', 'ContourNumber'],
8868            '0x0049': ['IS', '1-n', 'AttachedContours'],
8869            '0x0050': ['DS', '3-3n', 'ContourData'],
8870            '0x0080': ['SQ', '1', 'RTROIObservationsSequence'],
8871            '0x0082': ['IS', '1', 'ObservationNumber'],
8872            '0x0084': ['IS', '1', 'ReferencedROINumber'],
8873            '0x0085': ['SH', '1', 'ROIObservationLabel'],
8874            '0x0086': ['SQ', '1', 'RTROIIdentificationCodeSequence'],
8875            '0x0088': ['ST', '1', 'ROIObservationDescription'],
8876            '0x00A0': ['SQ', '1', 'RelatedRTROIObservationsSequence'],
8877            '0x00A4': ['CS', '1', 'RTROIInterpretedType'],
8878            '0x00A6': ['PN', '1', 'ROIInterpreter'],
8879            '0x00B0': ['SQ', '1', 'ROIPhysicalPropertiesSequence'],
8880            '0x00B2': ['CS', '1', 'ROIPhysicalProperty'],
8881            '0x00B4': ['DS', '1', 'ROIPhysicalPropertyValue'],
8882            '0x00B6': ['SQ', '1', 'ROIElementalCompositionSequence'],
8883            '0x00B7': ['US', '1', 'ROIElementalCompositionAtomicNumber'],
8884            '0x00B8': ['FL', '1', 'ROIElementalCompositionAtomicMassFraction'],
8885            '0x00B9': ['SQ', '1', 'AdditionalRTROIIdentificationCodeSequence'],
8886            '0x00C0': ['SQ', '1', 'FrameOfReferenceRelationshipSequence'],
8887            '0x00C2': ['UI', '1', 'RelatedFrameOfReferenceUID'],
8888            '0x00C4': ['CS', '1', 'FrameOfReferenceTransformationType'],
8889            '0x00C6': ['DS', '16', 'FrameOfReferenceTransformationMatrix'],
8890            '0x00C8': ['LO', '1', 'FrameOfReferenceTransformationComment']
8891        },
8892        '0x3008': {
8893            '0x0000': ['UL', '1', 'GenericGroupLength'],
8894            '0x0010': ['SQ', '1', 'MeasuredDoseReferenceSequence'],
8895            '0x0012': ['ST', '1', 'MeasuredDoseDescription'],
8896            '0x0014': ['CS', '1', 'MeasuredDoseType'],
8897            '0x0016': ['DS', '1', 'MeasuredDoseValue'],
8898            '0x0020': ['SQ', '1', 'TreatmentSessionBeamSequence'],
8899            '0x0021': ['SQ', '1', 'TreatmentSessionIonBeamSequence'],
8900            '0x0022': ['IS', '1', 'CurrentFractionNumber'],
8901            '0x0024': ['DA', '1', 'TreatmentControlPointDate'],
8902            '0x0025': ['TM', '1', 'TreatmentControlPointTime'],
8903            '0x002A': ['CS', '1', 'TreatmentTerminationStatus'],
8904            '0x002B': ['SH', '1', 'TreatmentTerminationCode'],
8905            '0x002C': ['CS', '1', 'TreatmentVerificationStatus'],
8906            '0x0030': ['SQ', '1', 'ReferencedTreatmentRecordSequence'],
8907            '0x0032': ['DS', '1', 'SpecifiedPrimaryMeterset'],
8908            '0x0033': ['DS', '1', 'SpecifiedSecondaryMeterset'],
8909            '0x0036': ['DS', '1', 'DeliveredPrimaryMeterset'],
8910            '0x0037': ['DS', '1', 'DeliveredSecondaryMeterset'],
8911            '0x003A': ['DS', '1', 'SpecifiedTreatmentTime'],
8912            '0x003B': ['DS', '1', 'DeliveredTreatmentTime'],
8913            '0x0040': ['SQ', '1', 'ControlPointDeliverySequence'],
8914            '0x0041': ['SQ', '1', 'IonControlPointDeliverySequence'],
8915            '0x0042': ['DS', '1', 'SpecifiedMeterset'],
8916            '0x0044': ['DS', '1', 'DeliveredMeterset'],
8917            '0x0045': ['FL', '1', 'MetersetRateSet'],
8918            '0x0046': ['FL', '1', 'MetersetRateDelivered'],
8919            '0x0047': ['FL', '1-n', 'ScanSpotMetersetsDelivered'],
8920            '0x0048': ['DS', '1', 'DoseRateDelivered'],
8921            '0x0050': ['SQ', '1', 'TreatmentSummaryCalculatedDoseReferenceSequence'],
8922            '0x0052': ['DS', '1', 'CumulativeDoseToDoseReference'],
8923            '0x0054': ['DA', '1', 'FirstTreatmentDate'],
8924            '0x0056': ['DA', '1', 'MostRecentTreatmentDate'],
8925            '0x005A': ['IS', '1', 'NumberOfFractionsDelivered'],
8926            '0x0060': ['SQ', '1', 'OverrideSequence'],
8927            '0x0061': ['AT', '1', 'ParameterSequencePointer'],
8928            '0x0062': ['AT', '1', 'OverrideParameterPointer'],
8929            '0x0063': ['IS', '1', 'ParameterItemIndex'],
8930            '0x0064': ['IS', '1', 'MeasuredDoseReferenceNumber'],
8931            '0x0065': ['AT', '1', 'ParameterPointer'],
8932            '0x0066': ['ST', '1', 'OverrideReason'],
8933            '0x0068': ['SQ', '1', 'CorrectedParameterSequence'],
8934            '0x006A': ['FL', '1', 'CorrectionValue'],
8935            '0x0070': ['SQ', '1', 'CalculatedDoseReferenceSequence'],
8936            '0x0072': ['IS', '1', 'CalculatedDoseReferenceNumber'],
8937            '0x0074': ['ST', '1', 'CalculatedDoseReferenceDescription'],
8938            '0x0076': ['DS', '1', 'CalculatedDoseReferenceDoseValue'],
8939            '0x0078': ['DS', '1', 'StartMeterset'],
8940            '0x007A': ['DS', '1', 'EndMeterset'],
8941            '0x0080': ['SQ', '1', 'ReferencedMeasuredDoseReferenceSequence'],
8942            '0x0082': ['IS', '1', 'ReferencedMeasuredDoseReferenceNumber'],
8943            '0x0090': ['SQ', '1', 'ReferencedCalculatedDoseReferenceSequence'],
8944            '0x0092': ['IS', '1', 'ReferencedCalculatedDoseReferenceNumber'],
8945            '0x00A0': ['SQ', '1', 'BeamLimitingDeviceLeafPairsSequence'],
8946            '0x00B0': ['SQ', '1', 'RecordedWedgeSequence'],
8947            '0x00C0': ['SQ', '1', 'RecordedCompensatorSequence'],
8948            '0x00D0': ['SQ', '1', 'RecordedBlockSequence'],
8949            '0x00E0': ['SQ', '1', 'TreatmentSummaryMeasuredDoseReferenceSequence'],
8950            '0x00F0': ['SQ', '1', 'RecordedSnoutSequence'],
8951            '0x00F2': ['SQ', '1', 'RecordedRangeShifterSequence'],
8952            '0x00F4': ['SQ', '1', 'RecordedLateralSpreadingDeviceSequence'],
8953            '0x00F6': ['SQ', '1', 'RecordedRangeModulatorSequence'],
8954            '0x0100': ['SQ', '1', 'RecordedSourceSequence'],
8955            '0x0105': ['LO', '1', 'SourceSerialNumber'],
8956            '0x0110': ['SQ', '1', 'TreatmentSessionApplicationSetupSequence'],
8957            '0x0116': ['CS', '1', 'ApplicationSetupCheck'],
8958            '0x0120': ['SQ', '1', 'RecordedBrachyAccessoryDeviceSequence'],
8959            '0x0122': ['IS', '1', 'ReferencedBrachyAccessoryDeviceNumber'],
8960            '0x0130': ['SQ', '1', 'RecordedChannelSequence'],
8961            '0x0132': ['DS', '1', 'SpecifiedChannelTotalTime'],
8962            '0x0134': ['DS', '1', 'DeliveredChannelTotalTime'],
8963            '0x0136': ['IS', '1', 'SpecifiedNumberOfPulses'],
8964            '0x0138': ['IS', '1', 'DeliveredNumberOfPulses'],
8965            '0x013A': ['DS', '1', 'SpecifiedPulseRepetitionInterval'],
8966            '0x013C': ['DS', '1', 'DeliveredPulseRepetitionInterval'],
8967            '0x0140': ['SQ', '1', 'RecordedSourceApplicatorSequence'],
8968            '0x0142': ['IS', '1', 'ReferencedSourceApplicatorNumber'],
8969            '0x0150': ['SQ', '1', 'RecordedChannelShieldSequence'],
8970            '0x0152': ['IS', '1', 'ReferencedChannelShieldNumber'],
8971            '0x0160': ['SQ', '1', 'BrachyControlPointDeliveredSequence'],
8972            '0x0162': ['DA', '1', 'SafePositionExitDate'],
8973            '0x0164': ['TM', '1', 'SafePositionExitTime'],
8974            '0x0166': ['DA', '1', 'SafePositionReturnDate'],
8975            '0x0168': ['TM', '1', 'SafePositionReturnTime'],
8976            '0x0171': ['SQ', '1', 'PulseSpecificBrachyControlPointDeliveredSequence'],
8977            '0x0172': ['US', '1', 'PulseNumber'],
8978            '0x0173': ['SQ', '1', 'BrachyPulseControlPointDeliveredSequence'],
8979            '0x0200': ['CS', '1', 'CurrentTreatmentStatus'],
8980            '0x0202': ['ST', '1', 'TreatmentStatusComment'],
8981            '0x0220': ['SQ', '1', 'FractionGroupSummarySequence'],
8982            '0x0223': ['IS', '1', 'ReferencedFractionNumber'],
8983            '0x0224': ['CS', '1', 'FractionGroupType'],
8984            '0x0230': ['CS', '1', 'BeamStopperPosition'],
8985            '0x0240': ['SQ', '1', 'FractionStatusSummarySequence'],
8986            '0x0250': ['DA', '1', 'TreatmentDate'],
8987            '0x0251': ['TM', '1', 'TreatmentTime']
8988        },
8989        '0x300A': {
8990            '0x0000': ['UL', '1', 'GenericGroupLength'],
8991            '0x0002': ['SH', '1', 'RTPlanLabel'],
8992            '0x0003': ['LO', '1', 'RTPlanName'],
8993            '0x0004': ['ST', '1', 'RTPlanDescription'],
8994            '0x0006': ['DA', '1', 'RTPlanDate'],
8995            '0x0007': ['TM', '1', 'RTPlanTime'],
8996            '0x0009': ['LO', '1-n', 'TreatmentProtocols'],
8997            '0x000A': ['CS', '1', 'PlanIntent'],
8998            '0x000B': ['LO', '1-n', 'TreatmentSites'],
8999            '0x000C': ['CS', '1', 'RTPlanGeometry'],
9000            '0x000E': ['ST', '1', 'PrescriptionDescription'],
9001            '0x0010': ['SQ', '1', 'DoseReferenceSequence'],
9002            '0x0012': ['IS', '1', 'DoseReferenceNumber'],
9003            '0x0013': ['UI', '1', 'DoseReferenceUID'],
9004            '0x0014': ['CS', '1', 'DoseReferenceStructureType'],
9005            '0x0015': ['CS', '1', 'NominalBeamEnergyUnit'],
9006            '0x0016': ['LO', '1', 'DoseReferenceDescription'],
9007            '0x0018': ['DS', '3', 'DoseReferencePointCoordinates'],
9008            '0x001A': ['DS', '1', 'NominalPriorDose'],
9009            '0x0020': ['CS', '1', 'DoseReferenceType'],
9010            '0x0021': ['DS', '1', 'ConstraintWeight'],
9011            '0x0022': ['DS', '1', 'DeliveryWarningDose'],
9012            '0x0023': ['DS', '1', 'DeliveryMaximumDose'],
9013            '0x0025': ['DS', '1', 'TargetMinimumDose'],
9014            '0x0026': ['DS', '1', 'TargetPrescriptionDose'],
9015            '0x0027': ['DS', '1', 'TargetMaximumDose'],
9016            '0x0028': ['DS', '1', 'TargetUnderdoseVolumeFraction'],
9017            '0x002A': ['DS', '1', 'OrganAtRiskFullVolumeDose'],
9018            '0x002B': ['DS', '1', 'OrganAtRiskLimitDose'],
9019            '0x002C': ['DS', '1', 'OrganAtRiskMaximumDose'],
9020            '0x002D': ['DS', '1', 'OrganAtRiskOverdoseVolumeFraction'],
9021            '0x0040': ['SQ', '1', 'ToleranceTableSequence'],
9022            '0x0042': ['IS', '1', 'ToleranceTableNumber'],
9023            '0x0043': ['SH', '1', 'ToleranceTableLabel'],
9024            '0x0044': ['DS', '1', 'GantryAngleTolerance'],
9025            '0x0046': ['DS', '1', 'BeamLimitingDeviceAngleTolerance'],
9026            '0x0048': ['SQ', '1', 'BeamLimitingDeviceToleranceSequence'],
9027            '0x004A': ['DS', '1', 'BeamLimitingDevicePositionTolerance'],
9028            '0x004B': ['FL', '1', 'SnoutPositionTolerance'],
9029            '0x004C': ['DS', '1', 'PatientSupportAngleTolerance'],
9030            '0x004E': ['DS', '1', 'TableTopEccentricAngleTolerance'],
9031            '0x004F': ['FL', '1', 'TableTopPitchAngleTolerance'],
9032            '0x0050': ['FL', '1', 'TableTopRollAngleTolerance'],
9033            '0x0051': ['DS', '1', 'TableTopVerticalPositionTolerance'],
9034            '0x0052': ['DS', '1', 'TableTopLongitudinalPositionTolerance'],
9035            '0x0053': ['DS', '1', 'TableTopLateralPositionTolerance'],
9036            '0x0055': ['CS', '1', 'RTPlanRelationship'],
9037            '0x0070': ['SQ', '1', 'FractionGroupSequence'],
9038            '0x0071': ['IS', '1', 'FractionGroupNumber'],
9039            '0x0072': ['LO', '1', 'FractionGroupDescription'],
9040            '0x0078': ['IS', '1', 'NumberOfFractionsPlanned'],
9041            '0x0079': ['IS', '1', 'NumberOfFractionPatternDigitsPerDay'],
9042            '0x007A': ['IS', '1', 'RepeatFractionCycleLength'],
9043            '0x007B': ['LT', '1', 'FractionPattern'],
9044            '0x0080': ['IS', '1', 'NumberOfBeams'],
9045            '0x0082': ['DS', '3', 'BeamDoseSpecificationPoint'],
9046            '0x0084': ['DS', '1', 'BeamDose'],
9047            '0x0086': ['DS', '1', 'BeamMeterset'],
9048            '0x0088': ['FL', '1', 'BeamDosePointDepth'],
9049            '0x0089': ['FL', '1', 'BeamDosePointEquivalentDepth'],
9050            '0x008A': ['FL', '1', 'BeamDosePointSSD'],
9051            '0x008B': ['CS', '1', 'BeamDoseMeaning'],
9052            '0x008C': ['SQ', '1', 'BeamDoseVerificationControlPointSequence'],
9053            '0x008D': ['FL', '1', 'AverageBeamDosePointDepth'],
9054            '0x008E': ['FL', '1', 'AverageBeamDosePointEquivalentDepth'],
9055            '0x008F': ['FL', '1', 'AverageBeamDosePointSSD'],
9056            '0x00A0': ['IS', '1', 'NumberOfBrachyApplicationSetups'],
9057            '0x00A2': ['DS', '3', 'BrachyApplicationSetupDoseSpecificationPoint'],
9058            '0x00A4': ['DS', '1', 'BrachyApplicationSetupDose'],
9059            '0x00B0': ['SQ', '1', 'BeamSequence'],
9060            '0x00B2': ['SH', '1', 'TreatmentMachineName'],
9061            '0x00B3': ['CS', '1', 'PrimaryDosimeterUnit'],
9062            '0x00B4': ['DS', '1', 'SourceAxisDistance'],
9063            '0x00B6': ['SQ', '1', 'BeamLimitingDeviceSequence'],
9064            '0x00B8': ['CS', '1', 'RTBeamLimitingDeviceType'],
9065            '0x00BA': ['DS', '1', 'SourceToBeamLimitingDeviceDistance'],
9066            '0x00BB': ['FL', '1', 'IsocenterToBeamLimitingDeviceDistance'],
9067            '0x00BC': ['IS', '1', 'NumberOfLeafJawPairs'],
9068            '0x00BE': ['DS', '3-n', 'LeafPositionBoundaries'],
9069            '0x00C0': ['IS', '1', 'BeamNumber'],
9070            '0x00C2': ['LO', '1', 'BeamName'],
9071            '0x00C3': ['ST', '1', 'BeamDescription'],
9072            '0x00C4': ['CS', '1', 'BeamType'],
9073            '0x00C5': ['FD', '1', 'BeamDeliveryDurationLimit'],
9074            '0x00C6': ['CS', '1', 'RadiationType'],
9075            '0x00C7': ['CS', '1', 'HighDoseTechniqueType'],
9076            '0x00C8': ['IS', '1', 'ReferenceImageNumber'],
9077            '0x00CA': ['SQ', '1', 'PlannedVerificationImageSequence'],
9078            '0x00CC': ['LO', '1-n', 'ImagingDeviceSpecificAcquisitionParameters'],
9079            '0x00CE': ['CS', '1', 'TreatmentDeliveryType'],
9080            '0x00D0': ['IS', '1', 'NumberOfWedges'],
9081            '0x00D1': ['SQ', '1', 'WedgeSequence'],
9082            '0x00D2': ['IS', '1', 'WedgeNumber'],
9083            '0x00D3': ['CS', '1', 'WedgeType'],
9084            '0x00D4': ['SH', '1', 'WedgeID'],
9085            '0x00D5': ['IS', '1', 'WedgeAngle'],
9086            '0x00D6': ['DS', '1', 'WedgeFactor'],
9087            '0x00D7': ['FL', '1', 'TotalWedgeTrayWaterEquivalentThickness'],
9088            '0x00D8': ['DS', '1', 'WedgeOrientation'],
9089            '0x00D9': ['FL', '1', 'IsocenterToWedgeTrayDistance'],
9090            '0x00DA': ['DS', '1', 'SourceToWedgeTrayDistance'],
9091            '0x00DB': ['FL', '1', 'WedgeThinEdgePosition'],
9092            '0x00DC': ['SH', '1', 'BolusID'],
9093            '0x00DD': ['ST', '1', 'BolusDescription'],
9094            '0x00DE': ['DS', '1', 'EffectiveWedgeAngle'],
9095            '0x00E0': ['IS', '1', 'NumberOfCompensators'],
9096            '0x00E1': ['SH', '1', 'MaterialID'],
9097            '0x00E2': ['DS', '1', 'TotalCompensatorTrayFactor'],
9098            '0x00E3': ['SQ', '1', 'CompensatorSequence'],
9099            '0x00E4': ['IS', '1', 'CompensatorNumber'],
9100            '0x00E5': ['SH', '1', 'CompensatorID'],
9101            '0x00E6': ['DS', '1', 'SourceToCompensatorTrayDistance'],
9102            '0x00E7': ['IS', '1', 'CompensatorRows'],
9103            '0x00E8': ['IS', '1', 'CompensatorColumns'],
9104            '0x00E9': ['DS', '2', 'CompensatorPixelSpacing'],
9105            '0x00EA': ['DS', '2', 'CompensatorPosition'],
9106            '0x00EB': ['DS', '1-n', 'CompensatorTransmissionData'],
9107            '0x00EC': ['DS', '1-n', 'CompensatorThicknessData'],
9108            '0x00ED': ['IS', '1', 'NumberOfBoli'],
9109            '0x00EE': ['CS', '1', 'CompensatorType'],
9110            '0x00EF': ['SH', '1', 'CompensatorTrayID'],
9111            '0x00F0': ['IS', '1', 'NumberOfBlocks'],
9112            '0x00F2': ['DS', '1', 'TotalBlockTrayFactor'],
9113            '0x00F3': ['FL', '1', 'TotalBlockTrayWaterEquivalentThickness'],
9114            '0x00F4': ['SQ', '1', 'BlockSequence'],
9115            '0x00F5': ['SH', '1', 'BlockTrayID'],
9116            '0x00F6': ['DS', '1', 'SourceToBlockTrayDistance'],
9117            '0x00F7': ['FL', '1', 'IsocenterToBlockTrayDistance'],
9118            '0x00F8': ['CS', '1', 'BlockType'],
9119            '0x00F9': ['LO', '1', 'AccessoryCode'],
9120            '0x00FA': ['CS', '1', 'BlockDivergence'],
9121            '0x00FB': ['CS', '1', 'BlockMountingPosition'],
9122            '0x00FC': ['IS', '1', 'BlockNumber'],
9123            '0x00FE': ['LO', '1', 'BlockName'],
9124            '0x0100': ['DS', '1', 'BlockThickness'],
9125            '0x0102': ['DS', '1', 'BlockTransmission'],
9126            '0x0104': ['IS', '1', 'BlockNumberOfPoints'],
9127            '0x0106': ['DS', '2-2n', 'BlockData'],
9128            '0x0107': ['SQ', '1', 'ApplicatorSequence'],
9129            '0x0108': ['SH', '1', 'ApplicatorID'],
9130            '0x0109': ['CS', '1', 'ApplicatorType'],
9131            '0x010A': ['LO', '1', 'ApplicatorDescription'],
9132            '0x010C': ['DS', '1', 'CumulativeDoseReferenceCoefficient'],
9133            '0x010E': ['DS', '1', 'FinalCumulativeMetersetWeight'],
9134            '0x0110': ['IS', '1', 'NumberOfControlPoints'],
9135            '0x0111': ['SQ', '1', 'ControlPointSequence'],
9136            '0x0112': ['IS', '1', 'ControlPointIndex'],
9137            '0x0114': ['DS', '1', 'NominalBeamEnergy'],
9138            '0x0115': ['DS', '1', 'DoseRateSet'],
9139            '0x0116': ['SQ', '1', 'WedgePositionSequence'],
9140            '0x0118': ['CS', '1', 'WedgePosition'],
9141            '0x011A': ['SQ', '1', 'BeamLimitingDevicePositionSequence'],
9142            '0x011C': ['DS', '2-2n', 'LeafJawPositions'],
9143            '0x011E': ['DS', '1', 'GantryAngle'],
9144            '0x011F': ['CS', '1', 'GantryRotationDirection'],
9145            '0x0120': ['DS', '1', 'BeamLimitingDeviceAngle'],
9146            '0x0121': ['CS', '1', 'BeamLimitingDeviceRotationDirection'],
9147            '0x0122': ['DS', '1', 'PatientSupportAngle'],
9148            '0x0123': ['CS', '1', 'PatientSupportRotationDirection'],
9149            '0x0124': ['DS', '1', 'TableTopEccentricAxisDistance'],
9150            '0x0125': ['DS', '1', 'TableTopEccentricAngle'],
9151            '0x0126': ['CS', '1', 'TableTopEccentricRotationDirection'],
9152            '0x0128': ['DS', '1', 'TableTopVerticalPosition'],
9153            '0x0129': ['DS', '1', 'TableTopLongitudinalPosition'],
9154            '0x012A': ['DS', '1', 'TableTopLateralPosition'],
9155            '0x012C': ['DS', '3', 'IsocenterPosition'],
9156            '0x012E': ['DS', '3', 'SurfaceEntryPoint'],
9157            '0x0130': ['DS', '1', 'SourceToSurfaceDistance'],
9158            '0x0131': ['FL', '1', 'AverageBeamDosePointSourceToExternalContourSurfaceDistance'],
9159            '0x0132': ['FL', '1', 'SourceToExternalContourDistance'],
9160            '0x0133': ['FL', '3', 'ExternalContourEntryPoint'],
9161            '0x0134': ['DS', '1', 'CumulativeMetersetWeight'],
9162            '0x0140': ['FL', '1', 'TableTopPitchAngle'],
9163            '0x0142': ['CS', '1', 'TableTopPitchRotationDirection'],
9164            '0x0144': ['FL', '1', 'TableTopRollAngle'],
9165            '0x0146': ['CS', '1', 'TableTopRollRotationDirection'],
9166            '0x0148': ['FL', '1', 'HeadFixationAngle'],
9167            '0x014A': ['FL', '1', 'GantryPitchAngle'],
9168            '0x014C': ['CS', '1', 'GantryPitchRotationDirection'],
9169            '0x014E': ['FL', '1', 'GantryPitchAngleTolerance'],
9170            '0x0180': ['SQ', '1', 'PatientSetupSequence'],
9171            '0x0182': ['IS', '1', 'PatientSetupNumber'],
9172            '0x0183': ['LO', '1', 'PatientSetupLabel'],
9173            '0x0184': ['LO', '1', 'PatientAdditionalPosition'],
9174            '0x0190': ['SQ', '1', 'FixationDeviceSequence'],
9175            '0x0192': ['CS', '1', 'FixationDeviceType'],
9176            '0x0194': ['SH', '1', 'FixationDeviceLabel'],
9177            '0x0196': ['ST', '1', 'FixationDeviceDescription'],
9178            '0x0198': ['SH', '1', 'FixationDevicePosition'],
9179            '0x0199': ['FL', '1', 'FixationDevicePitchAngle'],
9180            '0x019A': ['FL', '1', 'FixationDeviceRollAngle'],
9181            '0x01A0': ['SQ', '1', 'ShieldingDeviceSequence'],
9182            '0x01A2': ['CS', '1', 'ShieldingDeviceType'],
9183            '0x01A4': ['SH', '1', 'ShieldingDeviceLabel'],
9184            '0x01A6': ['ST', '1', 'ShieldingDeviceDescription'],
9185            '0x01A8': ['SH', '1', 'ShieldingDevicePosition'],
9186            '0x01B0': ['CS', '1', 'SetupTechnique'],
9187            '0x01B2': ['ST', '1', 'SetupTechniqueDescription'],
9188            '0x01B4': ['SQ', '1', 'SetupDeviceSequence'],
9189            '0x01B6': ['CS', '1', 'SetupDeviceType'],
9190            '0x01B8': ['SH', '1', 'SetupDeviceLabel'],
9191            '0x01BA': ['ST', '1', 'SetupDeviceDescription'],
9192            '0x01BC': ['DS', '1', 'SetupDeviceParameter'],
9193            '0x01D0': ['ST', '1', 'SetupReferenceDescription'],
9194            '0x01D2': ['DS', '1', 'TableTopVerticalSetupDisplacement'],
9195            '0x01D4': ['DS', '1', 'TableTopLongitudinalSetupDisplacement'],
9196            '0x01D6': ['DS', '1', 'TableTopLateralSetupDisplacement'],
9197            '0x0200': ['CS', '1', 'BrachyTreatmentTechnique'],
9198            '0x0202': ['CS', '1', 'BrachyTreatmentType'],
9199            '0x0206': ['SQ', '1', 'TreatmentMachineSequence'],
9200            '0x0210': ['SQ', '1', 'SourceSequence'],
9201            '0x0212': ['IS', '1', 'SourceNumber'],
9202            '0x0214': ['CS', '1', 'SourceType'],
9203            '0x0216': ['LO', '1', 'SourceManufacturer'],
9204            '0x0218': ['DS', '1', 'ActiveSourceDiameter'],
9205            '0x021A': ['DS', '1', 'ActiveSourceLength'],
9206            '0x021B': ['SH', '1', 'SourceModelID'],
9207            '0x021C': ['LO', '1', 'SourceDescription'],
9208            '0x0222': ['DS', '1', 'SourceEncapsulationNominalThickness'],
9209            '0x0224': ['DS', '1', 'SourceEncapsulationNominalTransmission'],
9210            '0x0226': ['LO', '1', 'SourceIsotopeName'],
9211            '0x0228': ['DS', '1', 'SourceIsotopeHalfLife'],
9212            '0x0229': ['CS', '1', 'SourceStrengthUnits'],
9213            '0x022A': ['DS', '1', 'ReferenceAirKermaRate'],
9214            '0x022B': ['DS', '1', 'SourceStrength'],
9215            '0x022C': ['DA', '1', 'SourceStrengthReferenceDate'],
9216            '0x022E': ['TM', '1', 'SourceStrengthReferenceTime'],
9217            '0x0230': ['SQ', '1', 'ApplicationSetupSequence'],
9218            '0x0232': ['CS', '1', 'ApplicationSetupType'],
9219            '0x0234': ['IS', '1', 'ApplicationSetupNumber'],
9220            '0x0236': ['LO', '1', 'ApplicationSetupName'],
9221            '0x0238': ['LO', '1', 'ApplicationSetupManufacturer'],
9222            '0x0240': ['IS', '1', 'TemplateNumber'],
9223            '0x0242': ['SH', '1', 'TemplateType'],
9224            '0x0244': ['LO', '1', 'TemplateName'],
9225            '0x0250': ['DS', '1', 'TotalReferenceAirKerma'],
9226            '0x0260': ['SQ', '1', 'BrachyAccessoryDeviceSequence'],
9227            '0x0262': ['IS', '1', 'BrachyAccessoryDeviceNumber'],
9228            '0x0263': ['SH', '1', 'BrachyAccessoryDeviceID'],
9229            '0x0264': ['CS', '1', 'BrachyAccessoryDeviceType'],
9230            '0x0266': ['LO', '1', 'BrachyAccessoryDeviceName'],
9231            '0x026A': ['DS', '1', 'BrachyAccessoryDeviceNominalThickness'],
9232            '0x026C': ['DS', '1', 'BrachyAccessoryDeviceNominalTransmission'],
9233            '0x0280': ['SQ', '1', 'ChannelSequence'],
9234            '0x0282': ['IS', '1', 'ChannelNumber'],
9235            '0x0284': ['DS', '1', 'ChannelLength'],
9236            '0x0286': ['DS', '1', 'ChannelTotalTime'],
9237            '0x0288': ['CS', '1', 'SourceMovementType'],
9238            '0x028A': ['IS', '1', 'NumberOfPulses'],
9239            '0x028C': ['DS', '1', 'PulseRepetitionInterval'],
9240            '0x0290': ['IS', '1', 'SourceApplicatorNumber'],
9241            '0x0291': ['SH', '1', 'SourceApplicatorID'],
9242            '0x0292': ['CS', '1', 'SourceApplicatorType'],
9243            '0x0294': ['LO', '1', 'SourceApplicatorName'],
9244            '0x0296': ['DS', '1', 'SourceApplicatorLength'],
9245            '0x0298': ['LO', '1', 'SourceApplicatorManufacturer'],
9246            '0x029C': ['DS', '1', 'SourceApplicatorWallNominalThickness'],
9247            '0x029E': ['DS', '1', 'SourceApplicatorWallNominalTransmission'],
9248            '0x02A0': ['DS', '1', 'SourceApplicatorStepSize'],
9249            '0x02A2': ['IS', '1', 'TransferTubeNumber'],
9250            '0x02A4': ['DS', '1', 'TransferTubeLength'],
9251            '0x02B0': ['SQ', '1', 'ChannelShieldSequence'],
9252            '0x02B2': ['IS', '1', 'ChannelShieldNumber'],
9253            '0x02B3': ['SH', '1', 'ChannelShieldID'],
9254            '0x02B4': ['LO', '1', 'ChannelShieldName'],
9255            '0x02B8': ['DS', '1', 'ChannelShieldNominalThickness'],
9256            '0x02BA': ['DS', '1', 'ChannelShieldNominalTransmission'],
9257            '0x02C8': ['DS', '1', 'FinalCumulativeTimeWeight'],
9258            '0x02D0': ['SQ', '1', 'BrachyControlPointSequence'],
9259            '0x02D2': ['DS', '1', 'ControlPointRelativePosition'],
9260            '0x02D4': ['DS', '3', 'ControlPoint3DPosition'],
9261            '0x02D6': ['DS', '1', 'CumulativeTimeWeight'],
9262            '0x02E0': ['CS', '1', 'CompensatorDivergence'],
9263            '0x02E1': ['CS', '1', 'CompensatorMountingPosition'],
9264            '0x02E2': ['DS', '1-n', 'SourceToCompensatorDistance'],
9265            '0x02E3': ['FL', '1', 'TotalCompensatorTrayWaterEquivalentThickness'],
9266            '0x02E4': ['FL', '1', 'IsocenterToCompensatorTrayDistance'],
9267            '0x02E5': ['FL', '1', 'CompensatorColumnOffset'],
9268            '0x02E6': ['FL', '1-n', 'IsocenterToCompensatorDistances'],
9269            '0x02E7': ['FL', '1', 'CompensatorRelativeStoppingPowerRatio'],
9270            '0x02E8': ['FL', '1', 'CompensatorMillingToolDiameter'],
9271            '0x02EA': ['SQ', '1', 'IonRangeCompensatorSequence'],
9272            '0x02EB': ['LT', '1', 'CompensatorDescription'],
9273            '0x0302': ['IS', '1', 'RadiationMassNumber'],
9274            '0x0304': ['IS', '1', 'RadiationAtomicNumber'],
9275            '0x0306': ['SS', '1', 'RadiationChargeState'],
9276            '0x0308': ['CS', '1', 'ScanMode'],
9277            '0x030A': ['FL', '2', 'VirtualSourceAxisDistances'],
9278            '0x030C': ['SQ', '1', 'SnoutSequence'],
9279            '0x030D': ['FL', '1', 'SnoutPosition'],
9280            '0x030F': ['SH', '1', 'SnoutID'],
9281            '0x0312': ['IS', '1', 'NumberOfRangeShifters'],
9282            '0x0314': ['SQ', '1', 'RangeShifterSequence'],
9283            '0x0316': ['IS', '1', 'RangeShifterNumber'],
9284            '0x0318': ['SH', '1', 'RangeShifterID'],
9285            '0x0320': ['CS', '1', 'RangeShifterType'],
9286            '0x0322': ['LO', '1', 'RangeShifterDescription'],
9287            '0x0330': ['IS', '1', 'NumberOfLateralSpreadingDevices'],
9288            '0x0332': ['SQ', '1', 'LateralSpreadingDeviceSequence'],
9289            '0x0334': ['IS', '1', 'LateralSpreadingDeviceNumber'],
9290            '0x0336': ['SH', '1', 'LateralSpreadingDeviceID'],
9291            '0x0338': ['CS', '1', 'LateralSpreadingDeviceType'],
9292            '0x033A': ['LO', '1', 'LateralSpreadingDeviceDescription'],
9293            '0x033C': ['FL', '1', 'LateralSpreadingDeviceWaterEquivalentThickness'],
9294            '0x0340': ['IS', '1', 'NumberOfRangeModulators'],
9295            '0x0342': ['SQ', '1', 'RangeModulatorSequence'],
9296            '0x0344': ['IS', '1', 'RangeModulatorNumber'],
9297            '0x0346': ['SH', '1', 'RangeModulatorID'],
9298            '0x0348': ['CS', '1', 'RangeModulatorType'],
9299            '0x034A': ['LO', '1', 'RangeModulatorDescription'],
9300            '0x034C': ['SH', '1', 'BeamCurrentModulationID'],
9301            '0x0350': ['CS', '1', 'PatientSupportType'],
9302            '0x0352': ['SH', '1', 'PatientSupportID'],
9303            '0x0354': ['LO', '1', 'PatientSupportAccessoryCode'],
9304            '0x0356': ['FL', '1', 'FixationLightAzimuthalAngle'],
9305            '0x0358': ['FL', '1', 'FixationLightPolarAngle'],
9306            '0x035A': ['FL', '1', 'MetersetRate'],
9307            '0x0360': ['SQ', '1', 'RangeShifterSettingsSequence'],
9308            '0x0362': ['LO', '1', 'RangeShifterSetting'],
9309            '0x0364': ['FL', '1', 'IsocenterToRangeShifterDistance'],
9310            '0x0366': ['FL', '1', 'RangeShifterWaterEquivalentThickness'],
9311            '0x0370': ['SQ', '1', 'LateralSpreadingDeviceSettingsSequence'],
9312            '0x0372': ['LO', '1', 'LateralSpreadingDeviceSetting'],
9313            '0x0374': ['FL', '1', 'IsocenterToLateralSpreadingDeviceDistance'],
9314            '0x0380': ['SQ', '1', 'RangeModulatorSettingsSequence'],
9315            '0x0382': ['FL', '1', 'RangeModulatorGatingStartValue'],
9316            '0x0384': ['FL', '1', 'RangeModulatorGatingStopValue'],
9317            '0x0386': ['FL', '1', 'RangeModulatorGatingStartWaterEquivalentThickness'],
9318            '0x0388': ['FL', '1', 'RangeModulatorGatingStopWaterEquivalentThickness'],
9319            '0x038A': ['FL', '1', 'IsocenterToRangeModulatorDistance'],
9320            '0x0390': ['SH', '1', 'ScanSpotTuneID'],
9321            '0x0392': ['IS', '1', 'NumberOfScanSpotPositions'],
9322            '0x0394': ['FL', '1-n', 'ScanSpotPositionMap'],
9323            '0x0396': ['FL', '1-n', 'ScanSpotMetersetWeights'],
9324            '0x0398': ['FL', '2', 'ScanningSpotSize'],
9325            '0x039A': ['IS', '1', 'NumberOfPaintings'],
9326            '0x03A0': ['SQ', '1', 'IonToleranceTableSequence'],
9327            '0x03A2': ['SQ', '1', 'IonBeamSequence'],
9328            '0x03A4': ['SQ', '1', 'IonBeamLimitingDeviceSequence'],
9329            '0x03A6': ['SQ', '1', 'IonBlockSequence'],
9330            '0x03A8': ['SQ', '1', 'IonControlPointSequence'],
9331            '0x03AA': ['SQ', '1', 'IonWedgeSequence'],
9332            '0x03AC': ['SQ', '1', 'IonWedgePositionSequence'],
9333            '0x0401': ['SQ', '1', 'ReferencedSetupImageSequence'],
9334            '0x0402': ['ST', '1', 'SetupImageComment'],
9335            '0x0410': ['SQ', '1', 'MotionSynchronizationSequence'],
9336            '0x0412': ['FL', '3', 'ControlPointOrientation'],
9337            '0x0420': ['SQ', '1', 'GeneralAccessorySequence'],
9338            '0x0421': ['SH', '1', 'GeneralAccessoryID'],
9339            '0x0422': ['ST', '1', 'GeneralAccessoryDescription'],
9340            '0x0423': ['CS', '1', 'GeneralAccessoryType'],
9341            '0x0424': ['IS', '1', 'GeneralAccessoryNumber'],
9342            '0x0425': ['FL', '1', 'SourceToGeneralAccessoryDistance'],
9343            '0x0431': ['SQ', '1', 'ApplicatorGeometrySequence'],
9344            '0x0432': ['CS', '1', 'ApplicatorApertureShape'],
9345            '0x0433': ['FL', '1', 'ApplicatorOpening'],
9346            '0x0434': ['FL', '1', 'ApplicatorOpeningX'],
9347            '0x0435': ['FL', '1', 'ApplicatorOpeningY'],
9348            '0x0436': ['FL', '1', 'SourceToApplicatorMountingPositionDistance'],
9349            '0x0440': ['IS', '1', 'NumberOfBlockSlabItems'],
9350            '0x0441': ['SQ', '1', 'BlockSlabSequence'],
9351            '0x0442': ['DS', '1', 'BlockSlabThickness'],
9352            '0x0443': ['US', '1', 'BlockSlabNumber'],
9353            '0x0450': ['SQ', '1', 'DeviceMotionControlSequence'],
9354            '0x0451': ['CS', '1', 'DeviceMotionExecutionMode'],
9355            '0x0452': ['CS', '1', 'DeviceMotionObservationMode'],
9356            '0x0453': ['SQ', '1', 'DeviceMotionParameterCodeSequence']
9357        },
9358        '0x300C': {
9359            '0x0000': ['UL', '1', 'GenericGroupLength'],
9360            '0x0002': ['SQ', '1', 'ReferencedRTPlanSequence'],
9361            '0x0004': ['SQ', '1', 'ReferencedBeamSequence'],
9362            '0x0006': ['IS', '1', 'ReferencedBeamNumber'],
9363            '0x0007': ['IS', '1', 'ReferencedReferenceImageNumber'],
9364            '0x0008': ['DS', '1', 'StartCumulativeMetersetWeight'],
9365            '0x0009': ['DS', '1', 'EndCumulativeMetersetWeight'],
9366            '0x000A': ['SQ', '1', 'ReferencedBrachyApplicationSetupSequence'],
9367            '0x000C': ['IS', '1', 'ReferencedBrachyApplicationSetupNumber'],
9368            '0x000E': ['IS', '1', 'ReferencedSourceNumber'],
9369            '0x0020': ['SQ', '1', 'ReferencedFractionGroupSequence'],
9370            '0x0022': ['IS', '1', 'ReferencedFractionGroupNumber'],
9371            '0x0040': ['SQ', '1', 'ReferencedVerificationImageSequence'],
9372            '0x0042': ['SQ', '1', 'ReferencedReferenceImageSequence'],
9373            '0x0050': ['SQ', '1', 'ReferencedDoseReferenceSequence'],
9374            '0x0051': ['IS', '1', 'ReferencedDoseReferenceNumber'],
9375            '0x0055': ['SQ', '1', 'BrachyReferencedDoseReferenceSequence'],
9376            '0x0060': ['SQ', '1', 'ReferencedStructureSetSequence'],
9377            '0x006A': ['IS', '1', 'ReferencedPatientSetupNumber'],
9378            '0x0080': ['SQ', '1', 'ReferencedDoseSequence'],
9379            '0x00A0': ['IS', '1', 'ReferencedToleranceTableNumber'],
9380            '0x00B0': ['SQ', '1', 'ReferencedBolusSequence'],
9381            '0x00C0': ['IS', '1', 'ReferencedWedgeNumber'],
9382            '0x00D0': ['IS', '1', 'ReferencedCompensatorNumber'],
9383            '0x00E0': ['IS', '1', 'ReferencedBlockNumber'],
9384            '0x00F0': ['IS', '1', 'ReferencedControlPointIndex'],
9385            '0x00F2': ['SQ', '1', 'ReferencedControlPointSequence'],
9386            '0x00F4': ['IS', '1', 'ReferencedStartControlPointIndex'],
9387            '0x00F6': ['IS', '1', 'ReferencedStopControlPointIndex'],
9388            '0x0100': ['IS', '1', 'ReferencedRangeShifterNumber'],
9389            '0x0102': ['IS', '1', 'ReferencedLateralSpreadingDeviceNumber'],
9390            '0x0104': ['IS', '1', 'ReferencedRangeModulatorNumber'],
9391            '0x0111': ['SQ', '1', 'OmittedBeamTaskSequence'],
9392            '0x0112': ['CS', '1', 'ReasonForOmission'],
9393            '0x0113': ['LO', '1', 'ReasonForOmissionDescription']
9394        },
9395        '0x300E': {
9396            '0x0000': ['UL', '1', 'GenericGroupLength'],
9397            '0x0002': ['CS', '1', 'ApprovalStatus'],
9398            '0x0004': ['DA', '1', 'ReviewDate'],
9399            '0x0005': ['TM', '1', 'ReviewTime'],
9400            '0x0008': ['PN', '1', 'ReviewerName']
9401        },
9402        '0x4000': {
9403            '0x0000': ['UL', '1', 'GenericGroupLength'],
9404            '0x0010': ['LT', '1', 'Arbitrary'],
9405            '0x4000': ['LT', '1', 'TextComments']
9406        },
9407        '0x4008': {
9408            '0x0000': ['UL', '1', 'GenericGroupLength'],
9409            '0x0040': ['SH', '1', 'ResultsID'],
9410            '0x0042': ['LO', '1', 'ResultsIDIssuer'],
9411            '0x0050': ['SQ', '1', 'ReferencedInterpretationSequence'],
9412            '0x00FF': ['CS', '1', 'ReportProductionStatusTrial'],
9413            '0x0100': ['DA', '1', 'InterpretationRecordedDate'],
9414            '0x0101': ['TM', '1', 'InterpretationRecordedTime'],
9415            '0x0102': ['PN', '1', 'InterpretationRecorder'],
9416            '0x0103': ['LO', '1', 'ReferenceToRecordedSound'],
9417            '0x0108': ['DA', '1', 'InterpretationTranscriptionDate'],
9418            '0x0109': ['TM', '1', 'InterpretationTranscriptionTime'],
9419            '0x010A': ['PN', '1', 'InterpretationTranscriber'],
9420            '0x010B': ['ST', '1', 'InterpretationText'],
9421            '0x010C': ['PN', '1', 'InterpretationAuthor'],
9422            '0x0111': ['SQ', '1', 'InterpretationApproverSequence'],
9423            '0x0112': ['DA', '1', 'InterpretationApprovalDate'],
9424            '0x0113': ['TM', '1', 'InterpretationApprovalTime'],
9425            '0x0114': ['PN', '1', 'PhysicianApprovingInterpretation'],
9426            '0x0115': ['LT', '1', 'InterpretationDiagnosisDescription'],
9427            '0x0117': ['SQ', '1', 'InterpretationDiagnosisCodeSequence'],
9428            '0x0118': ['SQ', '1', 'ResultsDistributionListSequence'],
9429            '0x0119': ['PN', '1', 'DistributionName'],
9430            '0x011A': ['LO', '1', 'DistributionAddress'],
9431            '0x0200': ['SH', '1', 'InterpretationID'],
9432            '0x0202': ['LO', '1', 'InterpretationIDIssuer'],
9433            '0x0210': ['CS', '1', 'InterpretationTypeID'],
9434            '0x0212': ['CS', '1', 'InterpretationStatusID'],
9435            '0x0300': ['ST', '1', 'Impressions'],
9436            '0x4000': ['ST', '1', 'ResultsComments']
9437        },
9438        '0x4010': {
9439            '0x0000': ['UL', '1', 'GenericGroupLength'],
9440            '0x0001': ['CS', '1', 'LowEnergyDetectors'],
9441            '0x0002': ['CS', '1', 'HighEnergyDetectors'],
9442            '0x0004': ['SQ', '1', 'DetectorGeometrySequence'],
9443            '0x1001': ['SQ', '1', 'ThreatROIVoxelSequence'],
9444            '0x1004': ['FL', '3', 'ThreatROIBase'],
9445            '0x1005': ['FL', '3', 'ThreatROIExtents'],
9446            '0x1006': ['OB', '1', 'ThreatROIBitmap'],
9447            '0x1007': ['SH', '1', 'RouteSegmentID'],
9448            '0x1008': ['CS', '1', 'GantryType'],
9449            '0x1009': ['CS', '1', 'OOIOwnerType'],
9450            '0x100A': ['SQ', '1', 'RouteSegmentSequence'],
9451            '0x1010': ['US', '1', 'PotentialThreatObjectID'],
9452            '0x1011': ['SQ', '1', 'ThreatSequence'],
9453            '0x1012': ['CS', '1', 'ThreatCategory'],
9454            '0x1013': ['LT', '1', 'ThreatCategoryDescription'],
9455            '0x1014': ['CS', '1', 'ATDAbilityAssessm
9455ent'],
9456            '0x1015': ['CS', '1', 'ATDAssessmentFlag'],
9457            '0x1016': ['FL', '1', 'ATDAssessmentProbability'],
9458            '0x1017': ['FL', '1', 'Mass'],
9459            '0x1018': ['FL', '1', 'Density'],
9460            '0x1019': ['FL', '1', 'ZEffective'],
9461            '0x101A': ['SH', '1', 'BoardingPassID'],
9462            '0x101B': ['FL', '3', 'CenterOfMass'],
9463            '0x101C': ['FL', '3', 'CenterOfPTO'],
9464            '0x101D': ['FL', '6-n', 'BoundingPolygon'],
9465            '0x101E': ['SH', '1', 'RouteSegmentStartLocationID'],
9466            '0x101F': ['SH', '1', 'RouteSegmentEndLocationID'],
9467            '0x1020': ['CS', '1', 'RouteSegmentLocationIDType'],
9468            '0x1021': ['CS', '1-n', 'AbortReason'],
9469            '0x1023': ['FL', '1', 'VolumeOfPTO'],
9470            '0x1024': ['CS', '1', 'AbortFlag'],
9471            '0x1025': ['DT', '1', 'RouteSegmentStartTime'],
9472            '0x1026': ['DT', '1', 'RouteSegmentEndTime'],
9473            '0x1027': ['CS', '1', 'TDRType'],
9474            '0x1028': ['CS', '1', 'InternationalRouteSegment'],
9475            '0x1029': ['LO', '1-n', 'ThreatDetectionAlgorithmandVersion'],
9476            '0x102A': ['SH', '1', 'AssignedLocation'],
9477            '0x102B': ['DT', '1', 'AlarmDecisionTime'],
9478            '0x1031': ['CS', '1', 'AlarmDecision'],
9479            '0x1033': ['US', '1', 'NumberOfTotalObjects'],
9480            '0x1034': ['US', '1', 'NumberOfAlarmObjects'],
9481            '0x1037': ['SQ', '1', 'PTORepresentationSequence'],
9482            '0x1038': ['SQ', '1', 'ATDAssessmentSequence'],
9483            '0x1039': ['CS', '1', 'TIPType'],
9484            '0x103A': ['CS', '1', 'DICOSVersion'],
9485            '0x1041': ['DT', '1', 'OOIOwnerCreationTime'],
9486            '0x1042': ['CS', '1', 'OOIType'],
9487            '0x1043': ['FL', '3', 'OOISize'],
9488            '0x1044': ['CS', '1', 'AcquisitionStatus'],
9489            '0x1045': ['SQ', '1', 'BasisMaterialsCodeSequence'],
9490            '0x1046': ['CS', '1', 'PhantomType'],
9491            '0x1047': ['SQ', '1', 'OOIOwnerSequence'],
9492            '0x1048': ['CS', '1', 'ScanType'],
9493            '0x1051': ['LO', '1', 'ItineraryID'],
9494            '0x1052': ['SH', '1', 'ItineraryIDType'],
9495            '0x1053': ['LO', '1', 'ItineraryIDAssigningAuthority'],
9496            '0x1054': ['SH', '1', 'RouteID'],
9497            '0x1055': ['SH', '1', 'RouteIDAssigningAuthority'],
9498            '0x1056': ['CS', '1', 'InboundArrivalType'],
9499            '0x1058': ['SH', '1', 'CarrierID'],
9500            '0x1059': ['CS', '1', 'CarrierIDAssigningAuthority'],
9501            '0x1060': ['FL', '3', 'SourceOrientation'],
9502            '0x1061': ['FL', '3', 'SourcePosition'],
9503            '0x1062': ['FL', '1', 'BeltHeight'],
9504            '0x1064': ['SQ', '1', 'AlgorithmRoutingCodeSequence'],
9505            '0x1067': ['CS', '1', 'TransportClassification'],
9506            '0x1068': ['LT', '1', 'OOITypeDescriptor'],
9507            '0x1069': ['FL', '1', 'TotalProcessingTime'],
9508            '0x106C': ['OB', '1', 'DetectorCalibrationData'],
9509            '0x106D': ['CS', '1', 'AdditionalScreeningPerformed'],
9510            '0x106E': ['CS', '1', 'AdditionalInspectionSelectionCriteria'],
9511            '0x106F': ['SQ', '1', 'AdditionalInspectionMethodSequence'],
9512            '0x1070': ['CS', '1', 'AITDeviceType'],
9513            '0x1071': ['SQ', '1', 'QRMeasurementsSequence'],
9514            '0x1072': ['SQ', '1', 'TargetMaterialSequence'],
9515            '0x1073': ['FD', '1', 'SNRThreshold'],
9516            '0x1075': ['DS', '1', 'ImageScaleRepresentation'],
9517            '0x1076': ['SQ', '1', 'ReferencedPTOSequence'],
9518            '0x1077': ['SQ', '1', 'ReferencedTDRInstanceSequence'],
9519            '0x1078': ['ST', '1', 'PTOLocationDescription'],
9520            '0x1079': ['SQ', '1', 'AnomalyLocatorIndicatorSequence'],
9521            '0x107A': ['FL', '3', 'AnomalyLocatorIndicator'],
9522            '0x107B': ['SQ', '1', 'PTORegionSequence'],
9523            '0x107C': ['CS', '1', 'InspectionSelectionCriteria'],
9524            '0x107D': ['SQ', '1', 'SecondaryInspectionMethodSequence'],
9525            '0x107E': ['DS', '6', 'PRCSToRCSOrientation']
9526        },
9527        '0x4FFE': {
9528            '0x0000': ['UL', '1', 'GenericGroupLength'],
9529            '0x0001': ['SQ', '1', 'MACParametersSequence']
9530        },
9531        '0x5000': {
9532            '0x0000': ['UL', '1', 'GenericGroupLength'],
9533            '0x0005': ['US', '1', 'CurveDimensions'],
9534            '0x0010': ['US', '1', 'NumberOfPoints'],
9535            '0x0020': ['CS', '1', 'TypeOfData'],
9536            '0x0022': ['LO', '1', 'CurveDescription'],
9537            '0x0030': ['SH', '1-n', 'AxisUnits'],
9538            '0x0040': ['SH', '1-n', 'AxisLabels'],
9539            '0x0103': ['US', '1', 'DataValueRepresentation'],
9540            '0x0104': ['US', '1-n', 'MinimumCoordinateValue'],
9541            '0x0105': ['US', '1-n', 'MaximumCoordinateValue'],
9542            '0x0106': ['SH', '1-n', 'CurveRange'],
9543            '0x0110': ['US', '1-n', 'CurveDataDescriptor'],
9544            '0x0112': ['US', '1-n', 'CoordinateStartValue'],
9545            '0x0114': ['US', '1-n', 'CoordinateStepValue'],
9546            '0x1001': ['CS', '1', 'CurveActivationLayer'],
9547            '0x2000': ['US', '1', 'AudioType'],
9548            '0x2002': ['US', '1', 'AudioSampleFormat'],
9549            '0x2004': ['US', '1', 'NumberOfChannels'],
9550            '0x2006': ['UL', '1', 'NumberOfSamples'],
9551            '0x2008': ['UL', '1', 'SampleRate'],
9552            '0x200A': ['UL', '1', 'TotalTime'],
9553            '0x200C': ['ox', '1', 'AudioSampleData'],
9554            '0x200E': ['LT', '1', 'AudioComments'],
9555            '0x2500': ['LO', '1', 'CurveLabel'],
9556            '0x2600': ['SQ', '1', 'CurveReferencedOverlaySequence'],
9557            '0x2610': ['US', '1', 'CurveReferencedOverlayGroup'],
9558            '0x3000': ['ox', '1', 'CurveData']
9559        },
9560        '0x5200': {
9561            '0x0000': ['UL', '1', 'GenericGroupLength'],
9562            '0x9229': ['SQ', '1', 'SharedFunctionalGroupsSequence'],
9563            '0x9230': ['SQ', '1', 'PerFrameFunctionalGroupsSequence']
9564        },
9565        '0x5400': {
9566            '0x0000': ['UL', '1', 'GenericGroupLength'],
9567            '0x0100': ['SQ', '1', 'WaveformSequence'],
9568            '0x0110': ['ox', '1', 'ChannelMinimumValue'],
9569            '0x0112': ['ox', '1', 'ChannelMaximumValue'],
9570            '0x1004': ['US', '1', 'WaveformBitsAllocated'],
9571            '0x1006': ['CS', '1', 'WaveformSampleInterpretation'],
9572            '0x100A': ['ox', '1', 'WaveformPaddingValue'],
9573            '0x1010': ['ox', '1', 'WaveformData']
9574        },
9575        '0x5600': {
9576            '0x0000': ['UL', '1', 'GenericGroupLength'],
9577            '0x0010': ['OF', '1', 'FirstOrderPhaseCorrectionAngle'],
9578            '0x0020': ['OF', '1', 'SpectroscopyData']
9579        },
9580        '0x6000': {
9581            '0x0000': ['UL', '1', 'GenericGroupLength'],
9582            '0x0010': ['US', '1', 'OverlayRows'],
9583            '0x0011': ['US', '1', 'OverlayColumns'],
9584            '0x0012': ['US', '1', 'OverlayPlanes'],
9585            '0x0015': ['IS', '1', 'NumberOfFramesInOverlay'],
9586            '0x0022': ['LO', '1', 'OverlayDescription'],
9587            '0x0040': ['CS', '1', 'OverlayType'],
9588            '0x0045': ['LO', '1', 'OverlaySubtype'],
9589            '0x0050': ['SS', '2', 'OverlayOrigin'],
9590            '0x0051': ['US', '1', 'ImageFrameOrigin'],
9591            '0x0052': ['US', '1', 'OverlayPlaneOrigin'],
9592            '0x0060': ['CS', '1', 'OverlayCompressionCode'],
9593            '0x0061': ['SH', '1', 'OverlayCompressionOriginator'],
9594            '0x0062': ['SH', '1', 'OverlayCompressionLabel'],
9595            '0x0063': ['CS', '1', 'OverlayCompressionDescription'],
9596            '0x0066': ['AT', '1-n', 'OverlayCompressionStepPointers'],
9597            '0x0068': ['US', '1', 'OverlayRepeatInterval'],
9598            '0x0069': ['US', '1', 'OverlayBitsGrouped'],
9599            '0x0100': ['US', '1', 'OverlayBitsAllocated'],
9600            '0x0102': ['US', '1', 'OverlayBitPosition'],
9601            '0x0110': ['CS', '1', 'OverlayFormat'],
9602            '0x0200': ['US', '1', 'OverlayLocation'],
9603            '0x0800': ['CS', '1-n', 'OverlayCodeLabel'],
9604            '0x0802': ['US', '1', 'OverlayNumberOfTables'],
9605            '0x0803': ['AT', '1-n', 'OverlayCodeTableLocation'],
9606            '0x0804': ['US', '1', 'OverlayBitsForCodeWord'],
9607            '0x1001': ['CS', '1', 'OverlayActivationLayer'],
9608            '0x1100': ['US', '1', 'OverlayDescriptorGray'],
9609            '0x1101': ['US', '1', 'OverlayDescriptorRed'],
9610            '0x1102': ['US', '1', 'OverlayDescriptorGreen'],
9611            '0x1103': ['US', '1', 'OverlayDescriptorBlue'],
9612            '0x1200': ['US', '1-n', 'OverlaysGray'],
9613            '0x1201': ['US', '1-n', 'OverlaysRed'],
9614            '0x1202': ['US', '1-n', 'OverlaysGreen'],
9615            '0x1203': ['US', '1-n', 'OverlaysBlue'],
9616            '0x1301': ['IS', '1', 'ROIArea'],
9617            '0x1302': ['DS', '1', 'ROIMean'],
9618            '0x1303': ['DS', '1', 'ROIStandardDeviation'],
9619            '0x1500': ['LO', '1', 'OverlayLabel'],
9620            '0x3000': ['ox', '1', 'OverlayData'],
9621            '0x4000': ['LT', '1', 'OverlayComments']
9622        },
9623        '0x7FE0': {
9624            '0x0000': ['UL', '1', 'GenericGroupLength'],
9625            '0x0008': ['OF', '1', 'FloatPixelData'],
9626            '0x0009': ['OD', '1', 'DoubleFloatPixelData'],
9627            '0x0010': ['ox', '1', 'PixelData'],
9628            '0x0020': ['OW', '1', 'CoefficientsSDVN'],
9629            '0x0030': ['OW', '1', 'CoefficientsSDHN'],
9630            '0x0040': ['OW', '1', 'CoefficientsSDDN']
9631        },
9632        '0x7F00': {
9633            '0x0000': ['UL', '1', 'GenericGroupLength'],
9634            '0x0010': ['ox', '1', 'VariablePixelData'],
9635            '0x0011': ['US', '1', 'VariableNextDataGroup'],
9636            '0x0020': ['OW', '1', 'VariableCoefficientsSDVN'],
9637            '0x0030': ['OW', '1', 'VariableCoefficientsSDHN'],
9638            '0x0040': ['OW', '1', 'VariableCoefficientsSDDN']
9639        },
9640        '0xFFFA': {
9641            '0x0000': ['UL', '1', 'GenericGroupLength'],
9642            '0xFFFA': ['SQ', '1', 'DigitalSignaturesSequence']
9643        },
9644        '0xFFFC': {
9645            '0x0000': ['UL', '1', 'GenericGroupLength'],
9646            '0xFFFC': ['OB', '1', 'DataSetTrailingPadding']
9647        },
9648        '0xFFFE': {
9649            '0x0000': ['UL', '1', 'GenericGroupLength'],
9650            '0xE000': ['NONE', '1', 'Item'],
9651            '0xE00D': ['NONE', '1', 'ItemDelimitationItem'],
9652            '0xE0DD': ['NONE', '1', 'SequenceDelimitationItem']
9653        }
9654    }; // dwv.dicom.Dictionnary
9655
9656    // taken from gdcm-2.6.1\Source\DataDictionary\GroupName.dic
9657    // -> removed duplicates (commented)
9658    dwv.dicom.TagGroups = {
9659        'x0000': 'Command',
9660        'x0002': 'Meta Element',
9661        'x0004': 'File Set',
9662        //'x0004': 'Directory',
9663        'x0008': 'Identifying',
9664        'x0009': 'SPI Identifying',
9665        'x0010': 'Patient',
9666        'x0012': 'Clinical Trial',
9667        'x0018': 'Acquisition',
9668        'x0019': 'SPI Acquisition',
9669        'x0020': 'Image',
9670        'x0021': 'SPI Image',
9671        'x0022': 'Ophtalmology',
9672        'x0028': 'Image Presentation',
9673        'x0032': 'Study',
9674        'x0038': 'Visit',
9675        'x003A': 'Waveform',
9676        'x0040': 'Procedure',
9677        //'x0040': ''Modality Worklist',
9678        'x0042': 'Encapsulated Document',
9679        'x0050': 'Device Informations',
9680        //'x0050': 'XRay Angio Device',
9681        'x0054': 'Nuclear Medicine',
9682        'x0060': 'Histogram',
9683        'x0070': 'Presentation State',
9684        'x0072': 'Hanging Protocol',
9685        'x0088': 'Storage',
9686        //'x0088': 'Medicine',
9687        'x0100': 'Authorization',
9688        'x0400': 'Digital Signature',
9689        'x1000': 'Code Table',
9690        'x1010': 'Zonal Map',
9691        'x2000': 'Film Session',
9692        'x2010': 'Film Box',
9693        'x2020': 'Image Box',
9694        'x2030': 'Annotation',
9695        'x2040': 'Overlay Box',
9696        'x2050': 'Presentation LUT',
9697        'x2100': 'Print Job',
9698        'x2110': 'Printer',
9699        'x2120': 'Queue',
9700        'x2130': 'Print Content',
9701        'x2200': 'Media Creation',
9702        'x3002': 'RT Image',
9703        'x3004': 'RT Dose',
9704        'x3006': 'RT StructureSet',
9705        'x3008': 'RT Treatment',
9706        'x300A': 'RT Plan',
9707        'x300C': 'RT Relationship',
9708        'x300E': 'RT Approval',
9709        'x4000': 'Text',
9710        'x4008': 'Results',
9711        'x4FFE': 'MAC Parameters',
9712        'x5000': 'Curve',
9713        'x5002': 'Curve',
9714        'x5004': 'Curve',
9715        'x5006': 'Curve',
9716        'x5008': 'Curve',
9717        'x500A': 'Curve',
9718        'x500C': 'Curve',
9719        'x500E': 'Curve',
9720        'x5400': 'Waveform Data',
9721        'x6000': 'Overlays',
9722        'x6002': 'Overlays',
9723        'x6004': 'Overlays',
9724        'x6008': 'Overlays',
9725        'x600A': 'Overlays',
9726        'x600C': 'Overlays',
9727        'x600E': 'Overlays',
9728        'xFFFC': 'Generic',
9729        'x7FE0': 'Pixel Data',
9730        'xFFFF': 'Unknown'
9731    };
9732
9733    // namespaces
9734    var dwv = dwv || {};
9735    /** @namespace */
9736    dwv.gui = dwv.gui || {};
9737    /** @namespace */
9738    dwv.gui.base = dwv.gui.base || {};
9739    /** @namespace */
9740    dwv.gui.filter = dwv.gui.filter || {};
9741    /** @namespace */
9742    dwv.gui.filter.base = dwv.gui.filter.base || {};
9743
9744    /**
9745     * Filter tool base gui.
9746     * @constructor
9747     */
9748    dwv.gui.base.Filter = function(app) {
9749        /**
9750         * Setup the filter tool HTML.
9751         */
9752        this.setup = function(list) {
9753            // filter select
9754            var filterSelector = dwv.html.createHtmlSelect("filterSelect", list, "filter");
9755            filterSelector.onchange = app.onChangeFilter;
9756
9757            // filter list element
9758            var filterLi = dwv.html.createHiddenElement("li", "filterLi");
9759            filterLi.className += " ui-block-b";
9760            filterLi.appendChild(filterSelector);
9761
9762            // append element
9763            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
9764            dwv.html.appendElement(node, filterLi);
9765        };
9766
9767        /**
9768         * Display the tool HTML.
9769         * @param {Boolean} flag True to display, false to hide.
9770         */
9771        this.display = function(flag) {
9772            var node = app.getElement("filterLi");
9773            dwv.html.displayElement(node, flag);
9774        };
9775
9776        /**
9777         * Initialise the tool HTML.
9778         */
9779        this.initialise = function() {
9780            // filter select: reset selected options
9781            var filterSelector = app.getElement("filterSelect");
9782            filterSelector.selectedIndex = 0;
9783            // refresh
9784            dwv.gui.refreshElement(filterSelector);
9785        };
9786
9787    }; // class dwv.gui.base.Filter
9788
9789    /**
9790     * Threshold filter base gui.
9791     * @constructor
9792     */
9793    dwv.gui.base.Threshold = function(app) {
9794        /**
9795         * Threshold slider.
9796         * @private
9797         * @type Object
9798         */
9799        var slider = new dwv.gui.Slider(app);
9800
9801        /**
9802         * Setup the threshold filter HTML.
9803         */
9804        this.setup = function() {
9805            // threshold list element
9806            var thresholdLi = dwv.html.createHiddenElement("li", "thresholdLi");
9807            thresholdLi.className += " ui-block-c";
9808
9809            // node
9810            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
9811            // append threshold
9812            node.appendChild(thresholdLi);
9813            // threshold slider
9814            slider.append();
9815            // refresh
9816            dwv.gui.refreshElement(node);
9817        };
9818
9819        /**
9820         * Clear the threshold filter HTML.
9821         * @param {Boolean} flag True to display, false to hide.
9822         */
9823        this.display = function(flag) {
9824            // only initialise at display time
9825            // (avoids min/max calculation at startup)
9826            if (flag) {
9827                slider.initialise();
9828            }
9829
9830            var node = app.getElement("thresholdLi");
9831            dwv.html.displayElement(node, flag);
9832        };
9833
9834        /**
9835         * Initialise the threshold filter HTML.
9836         */
9837        this.initialise = function() {
9838            // nothing to do
9839        };
9840
9841    }; // class dwv.gui.base.Threshold
9842
9843    /**
9844     * Create the apply filter button.
9845     */
9846    dwv.gui.filter.base.createFilterApplyButton = function(app) {
9847        var button = document.createElement("button");
9848        button.id = "runFilterButton";
9849        button.onclick = app.onRunFilter;
9850        button.setAttribute("style", "width:100%; margin-top:0.5em;");
9851        button.setAttribute("class", "ui-btn ui-btn-b");
9852        button.appendChild(document.createTextNode(dwv.i18n("basics.apply")));
9853        return button;
9854    };
9855
9856    /**
9857     * Sharpen filter base gui.
9858     * @constructor
9859     */
9860    dwv.gui.base.Sharpen = function(app) {
9861        /**
9862         * Setup the sharpen filter HTML.
9863         */
9864        this.setup = function() {
9865            // sharpen list element
9866            var sharpenLi = dwv.html.createHiddenElement("li", "sharpenLi");
9867            sharpenLi.className += " ui-block-c";
9868            sharpenLi.appendChild(dwv.gui.filter.base.createFilterApplyButton(app));
9869            // append element
9870            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
9871            dwv.html.appendElement(node, sharpenLi);
9872        };
9873
9874        /**
9875         * Display the sharpen filter HTML.
9876         * @param {Boolean} flag True to display, false to hide.
9877         */
9878        this.display = function(flag) {
9879            var node = app.getElement("sharpenLi");
9880            dwv.html.displayElement(node, flag);
9881        };
9882
9883    }; // class dwv.gui.base.Sharpen
9884
9885    /**
9886     * Sobel filter base gui.
9887     * @constructor
9888     */
9889    dwv.gui.base.Sobel = function(app) {
9890        /**
9891         * Setup the sobel filter HTML.
9892         */
9893        this.setup = function() {
9894            // sobel list element
9895            var sobelLi = dwv.html.createHiddenElement("li", "sobelLi");
9896            sobelLi.className += " ui-block-c";
9897            sobelLi.appendChild(dwv.gui.filter.base.createFilterApplyButton(app));
9898            // append element
9899            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
9900            dwv.html.appendElement(node, sobelLi);
9901        };
9902
9903        /**
9904         * Display the sobel filter HTML.
9905         * @param {Boolean} flag True to display, false to hide.
9906         */
9907        this.display = function(flag) {
9908            var node = app.getElement("sobelLi");
9909            dwv.html.displayElement(node, flag);
9910        };
9911
9912    }; // class dwv.gui.base.Sobel
9913
9914    // namespaces
9915    var dwv = dwv || {};
9916    dwv.gui = dwv.gui || {};
9917    dwv.gui.base = dwv.gui.base || {};
9918
9919    /**
9920     * Get the size of the image display window.
9921     */
9922    dwv.gui.base.getWindowSize = function() {
9923        return { 'width': window.innerWidth, 'height': window.innerHeight - 147 };
9924    };
9925
9926    /**
9927     * Ask some text to the user.
9928     * @param {String} message Text to display to the user.
9929     * @param {String} defaultText Default value displayed in the text input field.
9930     * @return {String} Text entered by the user.
9931     */
9932    dwv.gui.base.prompt = function(message, defaultText) {
9933        return prompt(message, defaultText);
9934    };
9935
9936    /**
9937     * Display a progress value.
9938     * @param {Number} percent The progress percentage.
9939     */
9940    dwv.gui.base.displayProgress = function( /*percent*/ ) {
9941        // default does nothing...
9942    };
9943
9944    /**
9945     * Focus the view on the image.
9946     */
9947    dwv.gui.base.focusImage = function() {
9948        // default does nothing...
9949    };
9950
9951    /**
9952     * Post process a HTML table.
9953     * @param {Object} table The HTML table to process.
9954     * @return The processed HTML table.
9955     */
9956    dwv.gui.base.postProcessTable = function( /*table*/ ) {
9957        // default does nothing...
9958    };
9959
9960    /**
9961     * Get a HTML element associated to a container div.
9962     * @param {Number} containerDivId The id of the container div.
9963     * @param {String} name The name or id to find.
9964     * @return {Object} The found element or null.
9965     */
9966    dwv.gui.base.getElement = function(containerDivId, name) {
9967        // get by class in the container div
9968        var parent = document.getElementById(containerDivId);
9969        var elements = parent.getElementsByClassName(name);
9970        // getting the last element since some libraries (ie jquery-mobile) creates
9971        // span in front of regular tags (such as select)...
9972        var element = elements[elements.length - 1];
9973        // if not found get by id with 'containerDivId-className'
9974        if (typeof element === "undefined") {
9975            element = document.getElementById(containerDivId + '-' + name);
9976        }
9977        return element;
9978    };
9979
9980    /**
9981     * Refresh a HTML element. Mainly for jquery-mobile.
9982     * @param {String} element The HTML element to refresh.
9983     */
9984    dwv.gui.base.refreshElement = function( /*element*/ ) {
9985        // base does nothing...
9986    };
9987
9988    /**
9989     * Set the selected item of a HTML select.
9990     * @param {String} element The HTML select element.
9991     * @param {String}
9991 value The value of the option to mark as selected.
9992     */
9993    dwv.gui.setSelected = function(element, value) {
9994        if (element) {
9995            var index = 0;
9996            for (index in element.options) {
9997                if (element.options[index].value === value) {
9998                    break;
9999                }
10000            }
10001            element.selectedIndex = index;
10002            dwv.gui.refreshElement(element);
10003        }
10004    };
10005
10006    /**
10007     * Slider base gui.
10008     * @constructor
10009     */
10010    dwv.gui.base.Slider = function(app) {
10011        /**
10012         * Append the slider HTML.
10013         */
10014        this.append = function() {
10015            // default values
10016            var min = 0;
10017            var max = 1;
10018
10019            // jquery-mobile range slider
10020            // minimum input
10021            var inputMin = document.createElement("input");
10022            inputMin.id = "threshold-min";
10023            inputMin.type = "range";
10024            inputMin.max = max;
10025            inputMin.min = min;
10026            inputMin.value = min;
10027            // maximum input
10028            var inputMax = document.createElement("input");
10029            inputMax.id = "threshold-max";
10030            inputMax.type = "range";
10031            inputMax.max = max;
10032            inputMax.min = min;
10033            inputMax.value = max;
10034            // slicer div
10035            var div = document.createElement("div");
10036            div.id = "threshold-div";
10037            div.setAttribute("data-role", "rangeslider");
10038            div.appendChild(inputMin);
10039            div.appendChild(inputMax);
10040            div.setAttribute("data-mini", "true");
10041            // append to document
10042            app.getElement("thresholdLi").appendChild(div);
10043            // bind change
10044            $("#threshold-div").on("change",
10045                function( /*event*/ ) {
10046                    app.onChangeMinMax({
10047                        "min": $("#threshold-min").val(),
10048                        "max": $("#threshold-max").val()
10049                    });
10050                }
10051            );
10052            // refresh
10053            dwv.gui.refreshElement(app.getElement("toolList"));
10054        };
10055
10056        /**
10057         * Initialise the slider HTML.
10058         */
10059        this.initialise = function() {
10060            var min = app.getImage().getDataRange().min;
10061            var max = app.getImage().getDataRange().max;
10062
10063            // minimum input
10064            var inputMin = document.getElementById("threshold-min");
10065            inputMin.max = max;
10066            inputMin.min = min;
10067            inputMin.value = min;
10068            // maximum input
10069            var inputMax = document.getElementById("threshold-max");
10070            inputMax.max = max;
10071            inputMax.min = min;
10072            inputMax.value = max;
10073            // refresh
10074            dwv.gui.refreshElement(app.getElement("toolList"));
10075        };
10076
10077    }; // class dwv.gui.base.Slider
10078
10079    /**
10080     * DICOM tags base gui.
10081     * @param {Object} app The associated application.
10082     * @constructor
10083     */
10084    dwv.gui.base.DicomTags = function(app) {
10085        /**
10086         * Update the DICOM tags table with the input info.
10087         * @param {Object} dataInfo The data information.
10088         */
10089        this.update = function(dataInfo) {
10090            // HTML node
10091            var node = app.getElement("tags");
10092            if (node === null) {
10093                console.warn("Cannot find a node to append the DICOM tags.");
10094                return;
10095            }
10096            // remove possible previous
10097            while (node.hasChildNodes()) {
10098                node.removeChild(node.firstChild);
10099            }
10100
10101            // exit if no tags
10102            if (dataInfo.length === 0) {
10103                console.warn("No DICOM tags to show.");
10104                return;
10105            }
10106
10107            // tags HTML table
10108            var table = dwv.html.toTable(dataInfo);
10109            table.className = "tagsTable";
10110
10111            // optional gui specific table post process
10112            dwv.gui.postProcessTable(table);
10113
10114            // check processed table
10115            if (table.rows.length === 0) {
10116                console.warn("The processed table does not contain data.");
10117                return;
10118            }
10119
10120            // translate first row
10121            dwv.html.translateTableRow(table.rows.item(0));
10122
10123            // append search form
10124            node.appendChild(dwv.html.getHtmlSearchForm(table));
10125            // append tags table
10126            node.appendChild(table);
10127
10128            // refresh
10129            dwv.gui.refreshElement(node);
10130        };
10131
10132    }; // class dwv.gui.base.DicomTags
10133
10134    /**
10135     * Drawing list base gui.
10136     * @param {Object} app The associated application.
10137     * @constructor
10138     */
10139    dwv.gui.base.DrawList = function(app) {
10140        /**
10141         * Closure to self.
10142         */
10143        var self = this;
10144
10145        /**
10146         * Update the draw list html element
10147         * @param {Object} event A change event, decides if the table is editable or not.
10148         */
10149        this.update = function(event) {
10150            var isEditable = false;
10151            if (typeof event.editable !== "undefined") {
10152                isEditable = event.editable;
10153            }
10154
10155            // HTML node
10156            var node = app.getElement("drawList");
10157            if (node === null) {
10158                console.warn("Cannot find a node to append the drawing list.");
10159                return;
10160            }
10161            // remove possible previous
10162            while (node.hasChildNodes()) {
10163                node.removeChild(node.firstChild);
10164            }
10165
10166            // drawing details
10167            var drawDisplayDetails = app.getDrawDisplayDetails();
10168
10169            // exit if no details
10170            if (drawDisplayDetails.length === 0) {
10171                return;
10172            }
10173
10174            // tags HTML table
10175            var table = dwv.html.toTable(drawDisplayDetails);
10176            table.className = "drawsTable";
10177
10178            // optional gui specific table post process
10179            dwv.gui.postProcessTable(table);
10180
10181            // check processed table
10182            if (table.rows.length === 0) {
10183                console.warn("The processed table does not contain data.");
10184                return;
10185            }
10186
10187            // translate first row
10188            dwv.html.translateTableRow(table.rows.item(0));
10189
10190            // translate shape names
10191            dwv.html.translateTableColumn(table, 3, "shape", "name");
10192
10193            // create a color onkeyup handler
10194            var createColorOnKeyUp = function(details) {
10195                return function() {
10196                    details.color = this.value;
10197                    app.updateDraw(details);
10198                };
10199            };
10200            // create a text onkeyup handler
10201            var createTextOnKeyUp = function(details) {
10202                return function() {
10203                    details.label = this.value;
10204                    app.updateDraw(details);
10205                };
10206            };
10207            // create a long text onkeyup handler
10208            var createLongTextOnKeyUp = function(details) {
10209                return function() {
10210                    details.description = this.value;
10211                    app.updateDraw(details);
10212                };
10213            };
10214            // create a row onclick handler
10215            var createRowOnClick = function(slice, frame) {
10216                return function() {
10217                    // update slice
10218                    var pos = app.getViewController().getCurrentPosition();
10219                    pos.k = slice;
10220                    app.getViewController().setCurrentPosition(pos);
10221                    // update frame
10222                    app.getViewController().setCurrentFrame(frame);
10223                    // focus on the image
10224                    dwv.gui.focusImage();
10225                };
10226            };
10227            // create visibility handler
10228            var createVisibleOnClick = function(details) {
10229                return function() {
10230                    app.toogleGroupVisibility(details);
10231                };
10232            };
10233
10234            // append visible column to the header row
10235            var row0 = table.rows.item(0);
10236            var cell00 = row0.insertCell(0);
10237            cell00.outerHTML = "<th>" + dwv.i18n("basics.visible") + "</th>";
10238
10239            // loop through rows
10240            for (var r = 1; r < table.rows.length; ++r) {
10241                var drawId = r - 1;
10242                var drawDetails = drawDisplayDetails[drawId];
10243                var row = table.rows.item(r);
10244                var cells = row.cells;
10245
10246                // loop through cells
10247                for (var c = 0; c < cells.length; ++c) {
10248                    // show short ID
10249                    if (c === 0) {
10250                        cells[c].firstChild.data = cells[c].firstChild.data.substring(0, 5);
10251                    }
10252
10253                    if (isEditable) {
10254                        // color
10255                        if (c === 4) {
10256                            dwv.html.makeCellEditable(cells[c], createColorOnKeyUp(drawDetails), "color");
10257                        }
10258                        // text
10259                        else if (c === 5) {
10260                            dwv.html.makeCellEditable(cells[c], createTextOnKeyUp(drawDetails));
10261                        }
10262                        // long text
10263                        else if (c === 6) {
10264                            dwv.html.makeCellEditable(cells[c], createLongTextOnKeyUp(drawDetails));
10265                        }
10266                    } else {
10267                        // id: link to image
10268                        cells[0].onclick = createRowOnClick(
10269                            cells[1].firstChild.data,
10270                            cells[2].firstChild.data);
10271                        cells[0].onmouseover = dwv.html.setCursorToPointer;
10272                        cells[0].onmouseout = dwv.html.setCursorToDefault;
10273                        // color: just display the input color with no callback
10274                        if (c === 4) {
10275                            dwv.html.makeCellEditable(cells[c], null, "color");
10276                        }
10277                    }
10278                }
10279
10280                // append visible column
10281                var cell0 = row.insertCell(0);
10282                var input = document.createElement("input");
10283                input.setAttribute("type", "checkbox");
10284                input.checked = app.isGroupVisible(drawDetails);
10285                input.onclick = createVisibleOnClick(drawDetails);
10286                cell0.appendChild(input);
10287            }
10288
10289            // editable checkbox
10290            var tickBox = document.createElement("input");
10291            tickBox.setAttribute("type", "checkbox");
10292            tickBox.id = "checkbox-editable";
10293            tickBox.checked = isEditable;
10294            tickBox.onclick = function() { self.update({ "editable": this.checked }); };
10295            // checkbox label
10296            var tickLabel = document.createElement("label");
10297            tickLabel.setAttribute("for", tickBox.id);
10298            tickLabel.setAttribute("class", "inline");
10299            tickLabel.appendChild(document.createTextNode(dwv.i18n("basics.editMode")));
10300            // checkbox div
10301            var tickDiv = document.createElement("div");
10302            tickDiv.appendChild(tickLabel);
10303            tickDiv.appendChild(tickBox);
10304
10305            // search form
10306            node.appendChild(dwv.html.getHtmlSearchForm(table));
10307            // tick form
10308            node.appendChild(tickDiv);
10309
10310            // draw list table
10311            node.appendChild(table);
10312
10313            // delete draw button
10314            var deleteButton = document.createElement("button");
10315            deleteButton.onclick = function() { app.deleteDraws(); };
10316            deleteButton.setAttribute("class", "ui-btn ui-btn-inline");
10317            deleteButton.appendChild(document.createTextNode(dwv.i18n("basics.deleteDraws")));
10318            if (!isEditable) {
10319                deleteButton.style.display = "none";
10320            }
10321            node.appendChild(deleteButton);
10322
10323            // refresh
10324            dwv.gui.refreshElement(node);
10325        };
10326
10327    }; // class dwv.gui.base.DrawList
10328
10329    // namespaces
10330    var dwv = dwv || {};
10331    dwv.gui = dwv.gui || {};
10332    dwv.gui.base = dwv.gui.base || {};
10333
10334    /**
10335     * Append the version HTML.
10336     */
10337    dwv.gui.base.appendVersionHtml = function(version) {
10338        var nodes = document.getElementsByClassName("dwv-version");
10339        if (nodes) {
10340            for (var i = 0; i < nodes.length; ++i) {
10341                nodes[i].appendChild(document.createTextNode(version));
10342            }
10343        }
10344    };
10345
10346    /**
10347     * Build the help HTML.
10348     * @param {Boolean} mobile Flag for mobile or not environement.
10349     */
10350    dwv.gui.base.appendHelpHtml = function(toolList, mobile, app) {
10351        var actionType = "mouse";
10352        if (mobile) {
10353            actionType = "touch";
10354        }
10355
10356        var toolHelpDiv = document.createElement("div");
10357
10358        // current location
10359        var loc = window.location.pathname;
10360        var dir = loc.substring(0, loc.lastIndexOf('/'));
10361
10362        var tool = null;
10363        var tkeys = Object.keys(toolList);
10364        for (var t = 0; t < tkeys.length; ++t) {
10365            tool = toolList[tkeys[t]];
10366            // title
10367            var title = document.createElement("h3");
10368            title.appendChild(document.createTextNode(tool.getHelp().title));
10369            // doc div
10370            var docDiv = document.createElement("div");
10371            // brief
10372            var brief = document.createElement("p");
10373            brief.appendChild(document.createTextNode(tool.getHelp().brief));
10374            docDiv.appendChild(brief);
10375            // details
10376            if (tool.getHelp()[actionType]) {
10377                var keys = Object.keys(tool.getHelp()[actionType]);
10378                for (var i = 0; i < keys.length; ++i) {
10379                    var action = tool.getHelp()[actionType][keys[i]];
10380
10381                    var img = document.createElement("img");
10382                    img.src = dir + "/../../resources/help/" + keys[i] + ".png";
10383                    img.style.float = "left";
10384                    img.style.margin = "0px 15px 15px 0px";
10385
10386                    var br = document.createElement("br");
10387                    br.style.clear = "both";
10388
10389                    var para = document.createElement("p");
10390                    para.appendChild(img);
10391                    para.appendChild(document.createTextNode(action));
10392                    para.appendChild(br);
10393                    docDiv.appendChild(para);
10394                }
10395            }
10396
10397            // different div structure for mobile or static
10398            if (mobile) {
10399                var toolDiv = document.createElement("div");
10400                toolDiv.setAttribute("data-role", "collapsible");
10401                toolDiv.appendChild(title);
10402                toolDiv.appendChild(docDiv);
10403                toolHelpDiv.appendChild(toolDiv);
10404            } else {
10405                toolHelpDiv.id = "accordion";
10406                toolHelpDiv.appendChild(title);
10407                toolHelpDiv.appendChild(docDiv);
10408            }
10409        }
10410
10411        var helpNode = app.getElement("help");
10412
10413        var headPara = document.createElement("p");
10414        headPara.appendChild(document.createTextNode(dwv.i18n("help.intro.p0")));
10415        helpNode.appendChild(headPara);
10416
10417        var secondPara = document.createElement("p");
10418        secondPara.appendChild(document.createTextNode(dwv.i18n("help.intro.p1")));
10419        helpNode.appendChild(secondPara);
10420
10421        var toolPara = document.createElement("p");
10422        toolPara.appendChild(document.createTextNode(dwv.i18n("help.tool_intro")));
10423        helpNode.appendChild(toolPara);
10424        helpNode.appendChild(toolHelpDiv);
10425    };
10426
10427    // namespaces
10428    var dwv = dwv || {};
10429    /** @namespace */
10430    dwv.html = dwv.html || {};
10431
10432    /**
10433     * Append a cell to a given row.
10434     * @param {Object} row The row to append the cell to.
10435     * @param {Object} content The content of the cell.
10436     */
10437    dwv.html.appendCell = function(row, content) {
10438        var cell = row.insertCell(-1);
10439        var str = content;
10440        // special care for arrays
10441        if (content instanceof Array ||
10442            content instanceof Uint8Array || content instanceof Int8Array ||
10443            content instanceof Uint16Array || content instanceof Int16Array ||
10444            content instanceof Uint32Array) {
10445            if (content.length > 10) {
10446                content = Array.prototype.slice.call(content, 0, 10);
10447                content[10] = "...";
10448            }
10449            str = Array.prototype.join.call(content, ', ');
10450        }
10451        // append
10452        cell.appendChild(document.createTextNode(str));
10453    };
10454
10455    /**
10456     * Append a header cell to a given row.
10457     * @param {Object} row The row to append the header cell to.
10458     * @param {String} text The text of the header cell.
10459     */
10460    dwv.html.appendHCell = function(row, text) {
10461        var cell = document.createElement("th");
10462        cell.appendChild(document.createTextNode(text));
10463        row.appendChild(cell);
10464    };
10465
10466    /**
10467     * Append a row to an array.
10468     * @param {Object} table The HTML table to append a row to.
10469     * @param {Array} input The input row array.
10470     * @param {Number} level The depth level of the input array.
10471     * @param {Number} maxLevel The maximum depth level.
10472     * @param {String} rowHeader The content of the first cell of a row (mainly for objects).
10473     */
10474    dwv.html.appendRowForArray = function(table, input, level, maxLevel, rowHeader) {
10475        var row = null;
10476        // loop through
10477        for (var i = 0; i < input.length; ++i) {
10478            var value = input[i];
10479            // last level
10480            if (typeof value === 'number' ||
10481                typeof value === 'string' ||
10482                value === null ||
10483                value === undefined ||
10484                level >= maxLevel) {
10485                if (!row) {
10486                    row = table.insertRow(-1);
10487                }
10488                dwv.html.appendCell(row, value);
10489            }
10490            // more to come
10491            else {
10492                dwv.html.appendRow(table, value, level + i, maxLevel, rowHeader);
10493            }
10494        }
10495    };
10496
10497    /**
10498     * Append a row to an object.
10499     * @param {Object} table The HTML table to append a row to.
10500     * @param {Array} input The input row array.
10501     * @param {Number} level The depth level of the input array.
10502     * @param {Number} maxLevel The maximum depth level.
10503     * @param {String} rowHeader The content of the first cell of a row (mainly for objects).
10504     */
10505    dwv.html.appendRowForObject = function(table, input, level, maxLevel, rowHeader) {
10506        var keys = Object.keys(input);
10507        var row = null;
10508        for (var o = 0; o < keys.length; ++o) {
10509            var value = input[keys[o]];
10510            // last level
10511            if (typeof value === 'number' ||
10512                typeof value === 'string' ||
10513                value === null ||
10514                value === undefined ||
10515                level >= maxLevel) {
10516                if (!row) {
10517                    row = table.insertRow(-1);
10518                }
10519                if (o === 0 && rowHeader) {
10520                    dwv.html.appendCell(row, rowHeader);
10521                }
10522                dwv.html.appendCell(row, value);
10523            }
10524            // more to come
10525            else {
10526                dwv.html.appendRow(table, value, level + o, maxLevel, keys[o]);
10527            }
10528        }
10529        // header row
10530        // warn: need to create the header after the rest
10531        // otherwise the data will inserted in the thead...
10532        if (level === 2) {
10533            var header = table.createTHead();
10534            var th = header.insertRow(-1);
10535            if (rowHeader) {
10536                dwv.html.appendHCell(th, "");
10537            }
10538            for (var k = 0; k < keys.length; ++k) {
10539                dwv.html.appendHCell(th, keys[k]);
10540            }
10541        }
10542    };
10543
10544    /**
10545     * Append a row to an object or an array.
10546     * @param {Object} table The HTML table to append a row to.
10547     * @param {Array} input The input row array.
10548     * @param {Number} level The depth level of the input array.
10549     * @param {Number} maxLevel The maximum depth level.
10550     * @param {String} rowHeader The content of the first cell of a row (mainly for objects).
10551     */
10552    dwv.html.appendRow = function(table, input, level, maxLevel, rowHeader) {
10553        // array
10554        if (input instanceof Array) {
10555            dwv.html.appendRowForArray(table, input, level + 1, maxLevel, rowHeader);
10556        }
10557        // object
10558        else if (typeof input === 'object') {
10559            dwv.html.appendRowForObject(table, input, level + 1, maxLevel, rowHeader);
10560        } else {
10561            throw new Error("Unsupported input data type.");
10562        }
10563    };
10564
10565    /**
10566     * Converts the input to an HTML table.
10567     * @input {Mixed} input Allowed types are: array, array of object, object.
10568     * @return {Object} The created HTML table or null if the input is empty.
10569     * @warning Null is interpreted differently in browsers, firefox will not display it.
10570     */
10571    dwv.html.toTable = function(input) {
10572        // check content
10573        if (input.length === 0) {
10574            return null;
10575        }
10576
10577        var table = document.createElement('table');
10578        dwv.html.appendRow(table, input, 0, 2);
10579        return table;
10580    };
10581
10582    /**
10583     * Get an HTML search form.
10584     * @param {Object} htmlTableToSearch The table to do the search on.
10585     * @return {Object} The HTML search form.
10586     */
10587    dwv.html.getHtmlSearchForm = function(htmlTableToSearch) {
10588        // input
10589        var input = document.createElement("input");
10590        input.id = "table-search";
10591        // TODO Use new html5 search type
10592        //input.setAttribute("type", "search");
10593        input.onkeyup = function() {
10594            dwv.html.filterTable(input, htmlTableToSearch);
10595        };
10596        // label
10597        var label = document.createElement("label");
10598        label.setAttribute("for", input.id);
10599        label.appendChild(document.createTextNode(dwv.i18n("basics.search") + ": "));
10600        // form
10601        var form = document.createElement("form");
10602        form.setAttribute("class", "filter");
10603        form.onsubmit = function(event) {
10604            event.preventDefault();
10605        };
10606        form.appendChild(label);
10607        form.appendChild(input);
10608        // return
10609        return form;
10610    };
10611
10612    /**
10613     * Filter a table with a given parameter: sets the display css of rows to
10614     * true or false if it contains the term.
10615     * @param {String} term The term to filter the table with.
10616     * @param {Object} table The table to filter.
10617     */
10618    dwv.html.filterTable = function(term, table) {
10619        // de-highlight
10620        dwv.html.dehighlight(table);
10621        // split search terms
10622        var terms = term.value.toLowerCase().split(" ");
10623
10624        // search
10625        var text = 0;
10626        var display = 0;
10627        for (var r = 1; r < table.rows.length; ++r) {
10628            display = '';
10629            for (var i = 0; i < terms.length; ++i) {
10630                text = table.rows[r].innerHTML.replace(/<[^>]+>/g, "").toLowerCase();
10631                if (text.indexOf(terms[i]) < 0) {
10632                    display = 'none';
10633                } else {
10634                    if (terms[i].length) {
10635                        dwv.html.highlight(terms[i], table.rows[r]);
10636                    }
10637                }
10638                table.rows[r].style.display = display;
10639            }
10640        }
10641    };
10642
10643    /**
10644     * Transform back each
10645     * 'preText <span class="highlighted">term</span> postText'
10646     * into its original 'preText term postText'.
10647     * @param {Object} container The container to de-highlight.
10648     */
10649    dwv.html.dehighlight = function(container) {
10650        for (var i = 0; i < container.childNodes.length; i++) {
10651            var node = container.childNodes[i];
10652
10653            if (node.attributes &&
10654                node.attributes['class'] &&
10655                node.attributes['class'].value === 'highlighted') {
10656                node.parentNode.parentNode.replaceChild(
10657                    document.createTextNode(
10658                        node.parentNode.innerHTML.replace(/<[^>]+>/g, "")),
10659                    node.parentNode);
10660                // Stop here and process next parent
10661                return;
10662            } else if (node.nodeType !== 3) {
10663                // Keep going onto other elements
10664                dwv.html.dehighlight(node);
10665            }
10666        }
10667    };
10668
10669    /**
10670     * Create a
10671     * 'preText <span class="highlighted">term</span> postText'
10672     * around each search term.
10673     * @param {String} term The term to highlight.
10674     * @param {Object} container The container where to highlight the term.
10675     */
10676    dwv.html.highlight = function(term, container) {
10677        for (var i = 0; i < container.childNodes.length; i++) {
10678            var node = container.childNodes[i];
10679
10680            if (node.nodeType === 3) {
10681                // Text node
10682                var data = node.data;
10683                var data_low = data.toLowerCase();
10684                if (data_low.indexOf(term) >= 0) {
10685                    //term found!
10686                    var new_node = document.createElement('span');
10687                    node.parentNode.replaceChild(new_node, node);
10688
10689                    var result;
10690                    while ((result = data_low.indexOf(term)) !== -1) {
10691                        // before term
10692                        new_node.appendChild(document.createTextNode(
10693                            data.substr(0, result)));
10694                        // term
10695                        new_node.appendChild(dwv.html.createHighlightNode(
10696                            document.createTextNode(data.substr(
10697                                result, term.length))));
10698                        // reduce search string
10699                        data = data.substr(result + term.length);
10700                        data_low = data_low.substr(result + term.length);
10701                    }
10702                    new_node.appendChild(document.createTextNode(data));
10703                }
10704            } else {
10705                // Keep going onto other elements
10706                dwv.html.highlight(term, node);
10707            }
10708        }
10709    };
10710
10711    /**
10712     * Highlight a HTML node.
10713     * @param {Object} child The child to highlight.
10714     * @return {Object} The created HTML node.
10715     */
10716    dwv.html.createHighlightNode = function(child) {
10717        var node = document.createElement('span');
10718        node.setAttribute('class', 'highlighted');
10719        node.attributes['class'].value = 'highlighted';
10720        node.appendChild(child);
10721        return node;
10722    };
10723
10724    /**
10725     * Remove all children of a HTML node.
10726     * @param {Object} node The node to remove kids.
10727     */
10728    dwv.html.cleanNode = function(node) {
10729        // remove its children if node exists
10730        if (!node) {
10731            return;
10732        }
10733        while (node.hasChildNodes()) {
10734            node.removeChild(node.firstChild);
10735        }
10736    };
10737
10738    /**
10739     * Remove a HTML node and all its children.
10740     * @param {String} nodeId The string id of the node to delete.
10741     */
10742    dwv.html.removeNode = function(node) {
10743        // check node
10744        if (!node) {
10745            return;
10746        }
10747        // remove its children
10748        dwv.html.cleanNode(node);
10749        // remove it from its parent
10750        var top = node.parentNode;
10751        top.removeChild(node);
10752    };
10753
10754    /**
10755     * Remove a list of HTML nodes and all their children.
10756     * @param {Array} nodes The list of nodes to delete.
10757     */
10758    dwv.html.removeNodes = function(nodes) {
10759        for (var i = 0; i < nodes.length; ++i) {
10760            dwv.html.removeNode(nodes[i]);
10761        }
10762    };
10763
10764    /**
10765     * Translate the content of an HTML row.
10766     * @param {Object} row The HTML row to parse.
10767     * @param {String} i18nPrefix The i18n prefix to use to find the translation.
10768     */
10769    dwv.html.translateTableRow = function(row, i18nPrefix) {
10770        var prefix = (typeof i18nPrefix === "undefined") ? "basics" : i18nPrefix;
10771        if (prefix.length !== 0) {
10772            prefix += ".";
10773        }
10774        var cells = row.cells;
10775        for (var c = 0; c < cells.length; ++c) {
10776            var text = cells[c].firstChild.data;
10777            cells[c].firstChild.data = dwv.i18n(prefix + text);
10778        }
10779    };
10780
10781    /**
10782     * Translate the content of an HTML column.
10783     * @param {Object} table The HTML table to parse.
10784     * @param {Number} columnNumber The number of the column to translate.
10785     * @param {String} i18nPrefix The i18n prefix to use to find the translation.
10786     * @param {String} i18nSuffix The i18n suffix to use to find the translation.
10787     */
10788    dwv.html.translateTableColumn = function(table, columnNumber, i18nPrefix, i18nSuffix) {
10789        var prefix = (typeof i18nPrefix === "undefined") ? "basics" : i18nPrefix;
10790        if (prefix.length !== 0) {
10791            prefix += ".";
10792        }
10793        var suffix = (typeof i18nSuffix === "undefined") ? "" : i18nSuffix;
10794        if (suffix.length !== 0) {
10795            suffix = "." + suffix;
10796        }
10797        if (table.rows.length !== 0) {
10798            for (var r = 1; r < table.rows.length; ++r) {
10799                var cells = table.rows.item(r).cells;
10800                if (cells.length >= columnNumber) {
10801                    var text = cells[columnNumber].firstChild.data;
10802                    cells[columnNumber].firstChild.data = dwv.i18n(prefix + text + suffix);
10803                }
10804            }
10805        }
10806    };
10807
10808    /**
10809     * Make a HTML table cell editable by putting its content inside an input element.
10810     * @param {Object} cell The cell to make editable.
10811     * @param {Function} onchange The callback to call when cell's content is changed.
10812     *    if set to null, the HTML input will be disabled.
10813     * @param {String} inputType The type of the HTML input, default to 'text'.
10814     */
10815    dwv.html.makeCellEditable = function(cell, onchange, inputType) {
10816        // check event
10817        if (typeof cell === "undefined") {
10818            console.warn("Cannot create input for non existing cell.");
10819            return;
10820        }
10821        // HTML input
10822        var input = document.createElement("input");
10823        // handle change
10824        if (onchange) {
10825            input.onchange = onchange;
10826        } else {
10827            input.disabled = true;
10828        }
10829        // set input value
10830        input.value = cell.firstChild.data;
10831        // input type
10832        if (typeof inputType === "undefined" ||
10833            (inputType === "color" && !dwv.browser.hasInputColor())) {
10834            input.type = "text";
10835        } else {
10836            input.type = inputType;
10837        }
10838
10839        // clean cell
10840        dwv.html.cleanNode(cell);
10841
10842        // HTML form
10843        var form = document.createElement("form");
10844        form.onsubmit = function(event) {
10845            event.preventDefault();
10846        };
10847        form.appendChild(input);
10848        // add form to cell
10849        cell.appendChild(form);
10850    };
10851
10852    /**
10853     * Set the document cursor to 'pointer'.
10854     */
10855    dwv.html.setCursorToPointer = function() {
10856        document.body.style.cursor = 'pointer';
10857    };
10858
10859    /**
10860     * Set the document cursor to 'default'.
10861     */
10862    dwv.html.setCursorToDefault = function() {
10863        document.body.style.cursor = 'default';
10864    };
10865
10866
10867    /**
10868     * Create a HTML select from an input array of options.
10869     * The values of the options are the name of the option made lower case.
10870     * It is left to the user to set the 'onchange' method of the select.
10871     * @param {String} name The name of the HTML select.
10872     * @param {Mixed} list The list of options of the HTML select.
10873     * @param {String} i18nPrefix An optional namespace prefix to find the translation values.
10874     * @param {Bool} i18nSafe An optional flag to check translation existence.
10875     * @return {Object} The created HTML select.
10876     */
10877    dwv.html.createHtmlSelect = function(name, list, i18nPrefix, i18nSafe) {
10878        // select
10879        var select = document.createElement("select");
10880        //select.name = name;
10881        select.className = name;
10882        var prefix = (typeof i18nPrefix === "undefined") ? "" : i18nPrefix + ".";
10883        var safe = (typeof i18nSafe === "undefined") ? false : true;
10884        var getText = function(value) {
10885            var key = prefix + value + ".name";
10886            var text = "";
10887            if (safe) {
10888                if (dwv.i18nExists(key)) {
10889                    text = dwv.i18n(key);
10890                } else {
10891                    text = value;
10892                }
10893            } else {
10894                text = dwv.i18n(key);
10895            }
10896            return text;
10897        };
10898        // options
10899        var option;
10900        if (list instanceof Array) {
10901            for (var i in list) {
10902                if (list.hasOwnProperty(i)) {
10903                    option = document.createElement("option");
10904                    option.value = list[i];
10905                    option.appendChild(document.createTextNode(getText(list[i])));
10906                    select.appendChild(option);
10907                }
10908            }
10909        } else if (typeof list === 'object') {
10910            for (var item in list) {
10911                option = document.createElement("option");
10912                option.value = item;
10913                option.appendChild(document.createTextNode(getText(item)));
10914                select.appendChild(option);
10915            }
10916        } else {
10917            throw new Error("Unsupported input list type.");
10918        }
10919        return select;
10920    };
10921
10922    /**
10923     * Display or not an element.
10924     * @param {Object} element The HTML element to display.
10925     * @param {Boolean} flag True to display the element.
10926     */
10927    dwv.html.displayElement = function(element, flag) {
10928        element.style.display = flag ? "" : "none";
10929    };
10930
10931    /**
10932     * Toggle the display of an element.
10933     * @param {Object} element The HTML element to display.
10934     */
10935    dwv.html.toggleDisplay = function(element) {
10936        if (element.style.display === "none") {
10937            element.style.display = '';
10938        } else {
10939            element.style.display = "none";
10940        }
10941    };
10942
10943    /**
10944     * Append an element.
10945     * @param {Object} parent The HTML element to append to.
10946     * @param {Object} element The HTML element to append.
10947     */
10948    dwv.html.appendElement = function(parent, element) {
10949        // append
10950        parent.appendChild(element);
10951        // refresh
10952        dwv.gui.refreshElement(parent);
10953    };
10954
10955    /**
10956     * Create an element.
10957     * @param {String} type The type of the elemnt.
10958     * @param {String} className The className of the element.
10959     */
10960    dwv.html.createHiddenElement = function(type, className) {
10961        var element = document.createElement(type);
10962        element.className = className;
10963        // hide by default
10964        element.style.display = "none";
10965        // return
10966        return element;
10967    };
10968
10969    // namespaces
10970    var dwv = dwv || {};
10971    dwv.gui = dwv.gui || {};
10972    dwv.gui.base = dwv.gui.base || {};
10973    dwv.gui.info = dwv.gui.info || {};
10974
10975    /**
10976     * Plot some data in a given div.
10977     * @param {Object} div The HTML element to add WindowLevel info to.
10978     * @param {Array} data The data array to plot.
10979     * @param {Object} options Plot options.
10980     */
10981    dwv.gui.base.plot = function( /*div, data, options*/ ) {
10982        // default does nothing...
10983    };
10984
10985    /**
10986     * MiniColourMap info layer.
10987     * @constructor
10988     * @param {Object} div The HTML element to add colourMap info to.
10989     * @param {Object} app The associated application.
10990     */
10991    dwv.gui.info.MiniColourMap = function(div, app) {
10992        /**
10993         * Create the mini colour map info div.
10994         */
10995        this.create = function() {
10996            // clean div
10997            var elems = div.getElementsByClassName("colour-map-info");
10998            if (elems.length !== 0) {
10999                dwv.html.removeNodes(elems);
11000            }
11001            // colour map
11002            var canvas = document.createElement("canvas");
11003            canvas.className = "colour-map-info";
11004            canvas.width = 98;
11005            canvas.height = 10;
11006            // add canvas to div
11007            div.appendChild(canvas);
11008        };
11009
11010        /**
11011         * Update the mini colour map info div.
11012         * @param {Object} event The windowing change event containing the new values.
11013         * Warning: expects the mini colour map div to exist (use after createMiniColourMap).
11014         */
11015        this.update = function(event) {
11016            var windowCenter = event.wc;
11017            var windowWidth = event.ww;
11018            // retrieve canvas and context
11019            var canvas = div.getElementsByClassName("colour-map-info")[0];
11020            var context = canvas.getContext('2d');
11021            // fill in the image data
11022            var colourMap = app.getViewController().getColourMap();
11023            var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
11024            // histogram sampling
11025            var c = 0;
11026            var minInt = app.getImage().getRescaledDataRange().min;
11027            var range = app.getImage().getRescaledDataRange().max - minInt;
11028            var incrC = range / canvas.width;
11029            // Y scale
11030            var y = 0;
11031            var yMax = 255;
11032            var yMin = 0;
11033            // X scale
11034            var xMin = windowCenter - 0.5 - (windowWidth - 1) / 2;
11035            var xMax = windowCenter - 0.5 + (windowWidth - 1) / 2;
11036            // loop through values
11037            var index;
11038            for (var j = 0; j < canvas.height; ++j) {
11039                c = minInt;
11040                for (var i = 0; i < canvas.width; ++i) {
11041                    if (c <= xMin) {
11042                        y = yMin;
11043                    } else if (c > xMax) {
11044                        y = yMax;
11045                    } else {
11046                        y = ((c - (windowCenter - 0.5)) / (windowWidth - 1) + 0.5) *
11047                            (yMax - yMin) + yMin;
11048                        y = parseInt(y, 10);
11049                    }
11050                    index = (i + j * canvas.width) * 4;
11051                    imageData.data[index] = colourMap.red[y];
11052                    imageData.data[index + 1] = colourMap.green[y];
11053                    imageData.data[index + 2] = colourMap.blue[y];
11054                    imageData.data[index + 3] = 0xff;
11055                    c += incrC;
11056                }
11057            }
11058            // put the image data in the context
11059            context.putImageData(imageData, 0, 0);
11060        };
11061    }; // class dwv.gui.info.MiniColourMap
11062
11063
11064    /**
11065     * Plot info layer.
11066     * @constructor
11067     * @param {Object} div The HTML element to add colourMap info to.
11068     * @param {Object} app The associated application.
11069     */
11070    dwv.gui.info.Plot = function(div, app) {
11071        /**
11072         * Create the plot info.
11073         */
11074        this.create = function() {
11075            // clean div
11076            if (div) {
11077                dwv.html.cleanNode(div);
11078            }
11079            // plot
11080            dwv.gui.plot(div, app.getImage().getHistogram());
11081        };
11082
11083        /**
11084         * Update plot.
11085         * @param {Object} event The windowing change event containing the new values.
11086         * Warning: expects the plot to exist (use after createPlot).
11087         */
11088        this.update = function(event) {
11089            var wc = event.wc;
11090            var ww = event.ww;
11091
11092            var half = parseInt((ww - 1) / 2, 10);
11093            var center = parseInt((wc - 0.5), 10);
11094            var min = center - half;
11095            var max = center + half;
11096
11097            var markings = [
11098                { "color": "#faa", "lineWidth": 1, "xaxis": { "from": min, "to": min } },
11099                { "color": "#aaf", "lineWidth": 1, "xaxis": { "from": max, "to": max } }
11100            ];
11101
11102            // plot
11103            dwv.gui.plot(div, app.getImage().getHistogram(), { markings: markings });
11104        };
11105
11106    }; // class dwv.gui.info.Plot
11107
11108    /**
11109     * DICOM Header overlay info layer.
11110     * @constructor
11111     * @param {Object} div The HTML element to add Header overlay info to.
11112     * @param {String} pos The string to specify the corner position. (tl,tc,tr,cl,cr,bl,bc,br)
11113     */
11114    dwv.gui.info.Overlay = function(div, pos, app) {
11115        // closure to self
11116        var self = this;
11117
11118        /**
11119         * Get the overlay array of the current position.
11120         * @return {Array} The overlay information.
11121         */
11122        this.getOverlays = function() {
11123            var image = app.getImage();
11124            if (!image) {
11125                return;
11126            }
11127            var allOverlays = image.getOverlays();
11128            if (!allOverlays) {
11129                return;
11130            }
11131            var position = app.getViewController().getCurrentPosition();
11132            var sliceOverlays = allOverlays[position.k];
11133            if (!sliceOverlays) {
11134                return;
11135            }
11136            return sliceOverlays[pos];
11137        };
11138
11139        /**
11140         * Create the overlay info div.
11141         */
11142        this.create = function() {
11143            // remove all <ul> elements from div
11144            dwv.html.cleanNode(div);
11145
11146            // get overlay string array of the current position
11147            var overlays = self.getOverlays();
11148            if (!overlays) {
11149                return;
11150            }
11151
11152            if (pos === "bc" || pos === "tc" ||
11153                pos === "cr" || pos === "cl") {
11154                div.textContent = overlays[0].value;
11155            } else {
11156                // create <ul> element
11157                var ul = document.createElement("ul");
11158
11159                for (var n = 0; overlays[n]; n++) {
11160                    var li;
11161                    if (overlays[n].value === "window-center") {
11162                        li = document.createElement("li");
11163                        li.className = "info-" + pos + "-window-center";
11164                        ul.appendChild(li);
11165                    } else if (overlays[n].value === "window-width") {
11166                        li = document.createElement("li");
11167                        li.className = "info-" + pos + "-window-width";
11168                        ul.appendChild(li);
11169                    } else if (overlays[n].value === "zoom") {
11170                        li = document.createElement("li");
11171                        li.className = "info-" + pos + "-zoom";
11172                        ul.appendChild(li);
11173                    } else if (overlays[n].value === "offset") {
11174                        li = document.createElement("li");
11175                        li.className = "info-" + pos + "-offset";
11176                        ul.appendChild(li);
11177                    } else if (overlays[n].value === "value") {
11178                        li = document.createElement("li");
11179                        li.className = "info-" + pos + "-value";
11180                        ul.appendChild(li);
11181                    } else if (overlays[n].value === "position") {
11182                        li = document.createElement("li");
11183                        li.className = "info-" + pos + "-position";
11184                        ul.appendChild(li);
11185                    } else if (overlays[n].value === "frame") {
11186                        li = document.createElement("li");
11187                        li.className = "info-" + pos + "-frame";
11188                        ul.appendChild(li);
11189                    } else {
11190                        li = document.createElement("li");
11191                        li.className = "info-" + pos + "-" + n;
11192                        li.appendChild(document.createTextNode(overlays[n].value));
11193                        ul.appendChild(li);
11194                    }
11195                }
11196
11197                // append <ul> element before color map
11198                div.appendChild(ul);
11199            }
11200        };
11201
11202        /**
11203         * Update the overlay info div.
11204         * @param {Object} event A change event.
11205         */
11206        this.update = function(event) {
11207            // get overlay string array of the current position
11208            var overlays = self.getOverlays();
11209            if (!overlays) {
11210                return;
11211            }
11212
11213            if (pos === "bc" || pos === "tc" ||
11214                pos === "cr" || pos === "cl") {
11215                div.textContent = overlays[0].value;
11216            } else {
11217                var li;
11218                var n;
11219
11220                for (n = 0; overlays[n]; n++) {
11221                    if (overlays[n].value === "window-center") {
11222                        if (event.type === "wl-center-change") {
11223                            li = div.getElementsByClassName("info-" + pos + "-window-center")[0];
11224                            dwv.html.cleanNode(li);
11225                            var wcStr = dwv.utils.replaceFlags2(overlays[n].format, [Math.round(event.wc)]);
11226                            if (li) {
11227                                li.appendChild(document.createTextNode(wcStr));
11228                            }
11229                        }
11230                    } else if (overlays[n].value === "window-width") {
11231                        if (event.type === "wl-width-change") {
11232                            li = div.getElementsByClassName("info-" + pos + "-window-width")[0];
11233                            dwv.html.cleanNode(li);
11234                            var wwStr = dwv.utils.replaceFlags2(overlays[n].format, [Math.round(event.ww)]);
11235                            if (li) {
11236                                li.appendChild(document.createTextNode(wwStr));
11237                            }
11238                        }
11239                    } else if (overlays[n].value === "zoom") {
11240                        if (event.type === "zoom-change") {
11241                            li = div.getElementsByClassName("info-" + pos + "-zoom")[0];
11242                            dwv.html.cleanNode(li);
11243                            var zoom = Number(event.scale).toPrecision(3);
11244                            var zoomStr = dwv.utils.replaceFlags2(overlays[n].format, [zoom]);
11245                            if (li) {
11246                                li.appendChild(document.createTextNode(zoomStr));
11247                            }
11248                        }
11249                    } else if (overlays[n].value === "offset") {
11250                        if (event.type === "zoom-change") {
11251                            li = div.getElementsByClassName("info-" + pos + "-offset")[0];
11252                            dwv.html.cleanNode(li);
11253                            var offset = [Number(event.cx).toPrecision(3),
11254                                Number(event.cy).toPrecision(3)
11255                            ];
11256                            var offStr = dwv.utils.replaceFlags2(overlays[n].format, offset);
11257                            if (li) {
11258                                li.appendChild(document.createTextNode(offStr));
11259                            }
11260                        }
11261                    } else if (overlays[n].value === "value") {
11262                        if (event.type === "position-change") {
11263                            li = div.getElementsByClassName("info-" + pos + "-value")[0];
11264                            dwv.html.cleanNode(li);
11265                            var valueStr = dwv.utils.replaceFlags2(overlays[n].format, [event.value]);
11266                            if (li) {
11267                                li.appendChild(document.createTextNode(valueStr));
11268                            }
11269                        }
11270                    } else if (overlays[n].value === "position") {
11271                        if (event.type === "position-change") {
11272                            li = div.getElementsByClassName("info-" + pos + "-position")[0];
11273                            dwv.html.cleanNode(li);
11274                            var posStr = dwv.utils.replaceFlags2(overlays[n].format, [event.i, event.j, event.k]);
11275                            if (li) {
11276                                li.appendChild(document.createTextNode(posStr));
11277                            }
11278                        }
11279                    } else if (overlays[n].value === "frame") {
11280                        if (event.type === "frame-change") {
11281                            li = div.getElementsByClassName("info-" + pos + "-frame")[0];
11282                            dwv.html.cleanNode(li);
11283                            var frameStr = dwv.utils.replaceFlags2(overlays[n].format, [event.frame]);
11284                            if (li) {
11285                                li.appendChild(document.createTextNode(frameStr));
11286                            }
11287                        }
11288                    } else {
11289                        if (event.type === "position-change") {
11290                            li = div.getElementsByClassName("info-" 
11290+ pos + "-" + n)[0];
11291                            dwv.html.cleanNode(li);
11292                            if (li) {
11293                                li.appendChild(document.createTextNode(overlays[n].value));
11294                            }
11295                        }
11296                    }
11297                }
11298            }
11299        };
11300    }; // class dwv.gui.info.Overlay
11301
11302    /**
11303     * Create overlay string array of the image in each corner
11304     * @param {Object} dicomElements DICOM elements of the image
11305     * @return {Array} Array of string to be shown in each corner
11306     */
11307    dwv.gui.info.createOverlays = function(dicomElements) {
11308        var overlays = {};
11309        var modality = dicomElements.getFromKey("x00080060");
11310        if (!modality) {
11311            return overlays;
11312        }
11313
11314        var omaps = dwv.gui.info.overlayMaps;
11315        if (!omaps) {
11316            return overlays;
11317        }
11318        var omap = omaps[modality] || omaps['*'];
11319
11320        for (var n = 0; omap[n]; n++) {
11321            var value = omap[n].value;
11322            var tags = omap[n].tags;
11323            var format = omap[n].format;
11324            var pos = omap[n].pos;
11325
11326            if (typeof tags !== "undefined" && tags.length !== 0) {
11327                // get values
11328                var values = [];
11329                for (var i = 0; i < tags.length; ++i) {
11330                    values.push(dicomElements.getElementValueAsStringFromKey(tags[i]));
11331                }
11332                // format
11333                if (typeof format === "undefined" || format === null) {
11334                    format = dwv.utils.createDefaultReplaceFormat(values);
11335                }
11336                value = dwv.utils.replaceFlags2(format, values);
11337            }
11338
11339            if (!value || value.length === 0) {
11340                continue;
11341            }
11342
11343            // add value to overlays
11344            if (!overlays[pos]) {
11345                overlays[pos] = [];
11346            }
11347            overlays[pos].push({ 'value': value.trim(), 'format': format });
11348        }
11349
11350        // (0020,0020) Patient Orientation
11351        var valuePO = dicomElements.getFromKey("x00200020");
11352        if (typeof valuePO !== "undefined" && valuePO !== null && valuePO.length == 2) {
11353            var po0 = dwv.dicom.cleanString(valuePO[0]);
11354            var po1 = dwv.dicom.cleanString(valuePO[1]);
11355            overlays.cr = [{ 'value': po0 }];
11356            overlays.cl = [{ 'value': dwv.dicom.getReverseOrientation(po0) }];
11357            overlays.bc = [{ 'value': po1 }];
11358            overlays.tc = [{ 'value': dwv.dicom.getReverseOrientation(po1) }];
11359        }
11360
11361        return overlays;
11362    };
11363
11364    /**
11365     * Create overlay string array of the image in each corner
11366     * @param {Object} dicomElements DICOM elements of the image
11367     * @return {Array} Array of string to be shown in each corner
11368     */
11369    dwv.gui.info.createOverlaysForDom = function(info) {
11370        var overlays = {};
11371        var omaps = dwv.gui.info.overlayMaps;
11372        if (!omaps) {
11373            return overlays;
11374        }
11375        var omap = omaps.DOM;
11376        if (!omap) {
11377            return overlays;
11378        }
11379
11380        for (var n = 0; omap[n]; n++) {
11381            var value = omap[n].value;
11382            var tags = omap[n].tags;
11383            var format = omap[n].format;
11384            var pos = omap[n].pos;
11385
11386            if (typeof tags !== "undefined" && tags.length !== 0) {
11387                // get values
11388                var values = [];
11389                for (var i = 0; i < tags.length; ++i) {
11390                    for (var j = 0; j < info.length; ++j) {
11391                        if (tags[i] === info[j].name) {
11392                            values.push(info[j].value);
11393                        }
11394                    }
11395                }
11396                // format
11397                if (typeof format === "undefined" || format === null) {
11398                    format = dwv.utils.createDefaultReplaceFormat(values);
11399                }
11400                value = dwv.utils.replaceFlags2(format, values);
11401            }
11402
11403            if (!value || value.length === 0) {
11404                continue;
11405            }
11406
11407            // add value to overlays
11408            if (!overlays[pos]) {
11409                overlays[pos] = [];
11410            }
11411            overlays[pos].push({ 'value': value.trim(), 'format': format });
11412        }
11413
11414        return overlays;
11415    };
11416
11417    // namespaces
11418    var dwv = dwv || {};
11419    dwv.html = dwv.html || {};
11420
11421    /**
11422     * Window layer.
11423     * @constructor
11424     * @param {String} name The name of the layer.
11425     */
11426    dwv.html.Layer = function(canvas) {
11427        /**
11428         * A cache of the initial canvas.
11429         * @private
11430         * @type Object
11431         */
11432        var cacheCanvas = null;
11433        /**
11434         * The associated CanvasRenderingContext2D.
11435         * @private
11436         * @type Object
11437         */
11438        var context = null;
11439
11440        /**
11441         * Get the layer canvas.
11442         * @return {Object} The layer canvas.
11443         */
11444        this.getCanvas = function() { return canvas; };
11445        /**
11446         * Get the layer context.
11447         * @return {Object} The layer context.
11448         */
11449        this.getContext = function() { return context; };
11450        /**
11451         * Get the layer offset on page.
11452         * @return {Number} The layer offset on page.
11453         */
11454        this.getOffset = function() { return canvas.offset(); };
11455
11456        /**
11457         * The image data array.
11458         * @private
11459         * @type Array
11460         */
11461        var imageData = null;
11462
11463        /**
11464         * The layer origin.
11465         * @private
11466         * @type {Object}
11467         */
11468        var origin = { 'x': 0, 'y': 0 };
11469        /**
11470         * Get the layer origin.
11471         * @return {Object} The layer origin as {'x','y'}.
11472         */
11473        this.getOrigin = function() {
11474            return origin;
11475        };
11476        /**
11477         * The layer zoom.
11478         * @private
11479         * @type {Object}
11480         */
11481        var zoom = { 'x': 1, 'y': 1 };
11482        /**
11483         * Get the layer zoom.
11484         * @return {Object} The layer zoom as {'x','y'}.
11485         */
11486        this.getZoom = function() {
11487            return zoom;
11488        };
11489
11490        /**
11491         * The layer translation.
11492         * @private
11493         * @type {Object}
11494         */
11495        var trans = { 'x': 0, 'y': 0 };
11496        /**
11497         * Get the layer translation.
11498         * @return {Object} The layer translation as {'x','y'}.
11499         */
11500        this.getTrans = function() {
11501            return trans;
11502        };
11503
11504        /**
11505         * Set the canvas width.
11506         * @param {Number} width The new width.
11507         */
11508        this.setWidth = function(width) {
11509            canvas.width = width;
11510        };
11511        /**
11512         * Set the canvas height.
11513         * @param {Number} height The new height.
11514         */
11515        this.setHeight = function(height) {
11516            canvas.height = height;
11517        };
11518
11519        /**
11520         * Set the layer zoom.
11521         * @param {Number} newZoomX The zoom in the X direction.
11522         * @param {Number} newZoomY The zoom in the Y direction.
11523         * @param {Number} centerX The zoom center in the X direction.
11524         * @param {Number} centerY The zoom center in the Y direction.
11525         */
11526        this.zoom = function(newZoomX, newZoomY, centerX, centerY) {
11527            // The zoom is the ratio between the differences from the center
11528            // to the origins:
11529            // centerX - originX = ( centerX - originX0 ) * zoomX
11530            // (center in ~world coordinate system)
11531            //originX = (centerX / zoomX) + originX - (centerX / newZoomX);
11532            //originY = (centerY / zoomY) + originY - (centerY / newZoomY);
11533
11534            // center in image coordinate system
11535            origin.x = centerX - (centerX - origin.x) * (newZoomX / zoom.x);
11536            origin.y = centerY - (centerY - origin.y) * (newZoomY / zoom.y);
11537
11538            // save zoom
11539            zoom.x = newZoomX;
11540            zoom.y = newZoomY;
11541        };
11542
11543        /**
11544         * Set the layer translation.
11545         * Translation is according to the last one.
11546         * @param {Number} tx The translation in the X direction.
11547         * @param {Number} ty The translation in the Y direction.
11548         */
11549        this.translate = function(tx, ty) {
11550            trans.x = tx;
11551            trans.y = ty;
11552        };
11553
11554        /**
11555         * Set the image data array.
11556         * @param {Array} data The data array.
11557         */
11558        this.setImageData = function(data) {
11559            imageData = data;
11560            // update the cached canvas
11561            cacheCanvas.getContext("2d").putImageData(imageData, 0, 0);
11562        };
11563
11564        /**
11565         * Reset the layout.
11566         */
11567        this.resetLayout = function(izoom) {
11568            origin.x = 0;
11569            origin.y = 0;
11570            zoom.x = izoom;
11571            zoom.y = izoom;
11572            trans.x = 0;
11573            trans.y = 0;
11574        };
11575
11576        /**
11577         * Transform a display position to an index.
11578         */
11579        this.displayToIndex = function(point2D) {
11580            return {
11581                'x': ((point2D.x - origin.x) / zoom.x) - trans.x,
11582                'y': ((point2D.y - origin.y) / zoom.y) - trans.y
11583            };
11584        };
11585
11586        /**
11587         * Draw the content (imageData) of the layer.
11588         * The imageData variable needs to be set
11589         */
11590        this.draw = function() {
11591            // clear the context: reset the transform first
11592            // store the current transformation matrix
11593            context.save();
11594            // use the identity matrix while clearing the canvas
11595            context.setTransform(1, 0, 0, 1, 0, 0);
11596            context.clearRect(0, 0, canvas.width, canvas.height);
11597            // restore the transform
11598            context.restore();
11599
11600            // draw the cached canvas on the context
11601            // transform takes as input a, b, c, d, e, f to create
11602            // the transform matrix (column-major order):
11603            // [ a c e ]
11604            // [ b d f ]
11605            // [ 0 0 1 ]
11606            context.setTransform(zoom.x, 0, 0, zoom.y,
11607                origin.x + (trans.x * zoom.x),
11608                origin.y + (trans.y * zoom.y));
11609            context.drawImage(cacheCanvas, 0, 0);
11610        };
11611
11612        /**
11613         * Initialise the layer: set the canvas and context
11614         * @input {Number} inputWidth The width of the canvas.
11615         * @input {Number} inputHeight The height of the canvas.
11616         */
11617        this.initialise = function(inputWidth, inputHeight) {
11618            // find the canvas element
11619            //canvas = document.getElementById(name);
11620            //if (!canvas)
11621            //{
11622            //    alert("Error: cannot find the canvas element for '" + name + "'.");
11623            //    return;
11624            //}
11625            // check that the getContext method exists
11626            if (!canvas.getContext) {
11627                alert("Error: no canvas.getContext method.");
11628                return;
11629            }
11630            // get the 2D context
11631            context = canvas.getContext('2d');
11632            if (!context) {
11633                alert("Error: failed to get the 2D context.");
11634                return;
11635            }
11636            // canvas sizes
11637            canvas.width = inputWidth;
11638            canvas.height = inputHeight;
11639            // original empty image data array
11640            context.clearRect(0, 0, canvas.width, canvas.height);
11641            imageData = context.getImageData(0, 0, canvas.width, canvas.height);
11642            // cached canvas
11643            cacheCanvas = document.createElement("canvas");
11644            cacheCanvas.width = inputWidth;
11645            cacheCanvas.height = inputHeight;
11646        };
11647
11648        /**
11649         * Fill the full context with the current style.
11650         */
11651        this.fillContext = function() {
11652            context.fillRect(0, 0, canvas.width, canvas.height);
11653        };
11654
11655        /**
11656         * Clear the context and reset the image data.
11657         */
11658        this.clear = function() {
11659            context.clearRect(0, 0, canvas.width, canvas.height);
11660            imageData = context.getImageData(0, 0, canvas.width, canvas.height);
11661            this.resetLayout();
11662        };
11663
11664        /**
11665         * Merge two layers.
11666         * @input {Layer} layerToMerge The layer to merge. It will also be emptied.
11667         */
11668        this.merge = function(layerToMerge) {
11669            // basic resampling of the merge data to put it at zoom 1:1
11670            var mergeImageData = layerToMerge.getContext().getImageData(
11671                0, 0, canvas.width, canvas.height);
11672            var offMerge = 0;
11673            var offMergeJ = 0;
11674            var offThis = 0;
11675            var offThisJ = 0;
11676            var alpha = 0;
11677            for (var j = 0; j < canvas.height; ++j) {
11678                offMergeJ = parseInt((origin.y + j * zoom.y), 10) * canvas.width;
11679                offThisJ = j * canvas.width;
11680                for (var i = 0; i < canvas.width; ++i) {
11681                    // 4 component data: RGB + alpha
11682                    offMerge = 4 * (parseInt((origin.x + i * zoom.x), 10) + offMergeJ);
11683                    offThis = 4 * (i + offThisJ);
11684                    // merge non transparent
11685                    alpha = mergeImageData.data[offMerge + 3];
11686                    if (alpha !== 0) {
11687                        imageData.data[offThis] = mergeImageData.data[offMerge];
11688                        imageData.data[offThis + 1] = mergeImageData.data[offMerge + 1];
11689                        imageData.data[offThis + 2] = mergeImageData.data[offMerge + 2];
11690                        imageData.data[offThis + 3] = alpha;
11691                    }
11692                }
11693            }
11694            // empty and reset merged layer
11695            layerToMerge.clear();
11696            // draw the layer
11697            this.draw();
11698        };
11699
11700        /**
11701         * Set the line colour for the layer.
11702         * @input {String} colour The line colour.
11703         */
11704        this.setLineColour = function(colour) {
11705            context.fillStyle = colour;
11706            context.strokeStyle = colour;
11707        };
11708
11709        /**
11710         * Display the layer.
11711         * @input {Boolean} val Whether to display the layer or not.
11712         */
11713        this.setStyleDisplay = function(val) {
11714            if (val === true) {
11715                canvas.style.display = '';
11716            } else {
11717                canvas.style.display = "none";
11718            }
11719        };
11720
11721        /**
11722         * Check if the layer is visible.
11723         * @return {Boolean} True if the layer is visible.
11724         */
11725        this.isVisible = function() {
11726            if (canvas.style.display === "none") {
11727                return false;
11728            } else {
11729                return true;
11730            }
11731        };
11732
11733        /**
11734         * Align on another layer.
11735         * @param {Layer} rhs The layer to align on.
11736         */
11737        this.align = function(rhs) {
11738            canvas.style.top = rhs.getCanvas().offsetTop;
11739            canvas.style.left = rhs.getCanvas().offsetLeft;
11740        };
11741    }; // Layer class
11742
11743    /**
11744     * Get the offset of an input event.
11745     * @param {Object} event The event to get the offset from.
11746     * @return {Array} The array of offsets.
11747     */
11748    dwv.html.getEventOffset = function(event) {
11749        var positions = [];
11750        var ex = 0;
11751        var ey = 0;
11752        if (event.targetTouches) {
11753            // get the touch offset from all its parents
11754            var offsetLeft = 0;
11755            var offsetTop = 0;
11756            var offsetParent = event.targetTouches[0].target.offsetParent;
11757            while (offsetParent) {
11758                if (!isNaN(offsetParent.offsetLeft)) {
11759                    offsetLeft += offsetParent.offsetLeft;
11760                }
11761                if (!isNaN(offsetParent.offsetTop)) {
11762                    offsetTop += offsetParent.offsetTop;
11763                }
11764                offsetParent = offsetParent.offsetParent;
11765            }
11766            // set its position
11767            var touch = null;
11768            for (var i = 0; i < event.targetTouches.length; ++i) {
11769                touch = event.targetTouches[i];
11770                ex = touch.pageX - offsetLeft;
11771                ey = touch.pageY - offsetTop;
11772                positions.push({ 'x': ex, 'y': ey });
11773            }
11774        } else {
11775            // layerX is used by Firefox
11776            ex = event.offsetX === undefined ? event.layerX : event.offsetX;
11777            ey = event.offsetY === undefined ? event.layerY : event.offsetY;
11778            positions.push({ 'x': ex, 'y': ey });
11779        }
11780        return positions;
11781    };
11782
11783    // namespaces
11784    var dwv = dwv || {};
11785    dwv.gui = dwv.gui || {};
11786    dwv.gui.base = dwv.gui.base || {};
11787
11788    /**
11789     * Loadbox base gui.
11790     * @constructor
11791     */
11792    dwv.gui.base.Loadbox = function(app, loaders) {
11793        /**
11794         * Loader HTML select.
11795         * @private
11796         */
11797        var loaderSelector = null;
11798
11799        /**
11800         * Setup the loadbox HTML.
11801         */
11802        this.setup = function() {
11803            // loader select
11804            loaderSelector = dwv.html.createHtmlSelect("loaderSelect", loaders, "io");
11805            loaderSelector.onchange = app.onChangeLoader;
11806
11807            // node
11808            var node = app.getElement("loaderlist");
11809            // clear it
11810            while (node.hasChildNodes()) {
11811                node.removeChild(node.firstChild);
11812            }
11813            // append
11814            node.appendChild(loaderSelector);
11815            // refresh
11816            dwv.gui.refreshElement(node);
11817        };
11818
11819        /**
11820         * Display a loader.
11821         * @param {String} name The name of the loader to show.
11822         */
11823        this.displayLoader = function(name) {
11824            var keys = Object.keys(loaders);
11825            for (var i = 0; i < keys.length; ++i) {
11826                if (keys[i] === name) {
11827                    loaders[keys[i]].display(true);
11828                } else {
11829                    loaders[keys[i]].display(false);
11830                }
11831            }
11832        };
11833
11834        /**
11835         * Reset to its original state.
11836         */
11837        this.reset = function() {
11838            // display first loader
11839            var keys = Object.keys(loaders);
11840            this.displayLoader(keys[0]);
11841            // reset HTML select
11842            if (loaderSelector) {
11843                loaderSelector.selectedIndex = 0;
11844            }
11845        };
11846
11847    }; // class dwv.gui.base.Loadbox
11848
11849    /**
11850     * FileLoad base gui.
11851     * @constructor
11852     */
11853    dwv.gui.base.FileLoad = function(app) {
11854        // closure to self
11855        var self = this;
11856
11857        /**
11858         * Internal file input change handler.
11859         * @param {Object} event The change event.
11860         */
11861        function onchangeinternal(event) {
11862            if (typeof self.onchange === "function") {
11863                self.onchange(event);
11864            }
11865            app.onChangeFiles(event);
11866        }
11867
11868        /**
11869         * Setup the file load HTML to the page.
11870         */
11871        this.setup = function() {
11872            // input
11873            var fileLoadInput = document.createElement("input");
11874            fileLoadInput.onchange = onchangeinternal;
11875            fileLoadInput.type = "file";
11876            fileLoadInput.multiple = true;
11877            fileLoadInput.className = "imagefiles";
11878            fileLoadInput.setAttribute("data-clear-btn", "true");
11879            fileLoadInput.setAttribute("data-mini", "true");
11880
11881            // associated div
11882            var fileLoadDiv = document.createElement("div");
11883            fileLoadDiv.className = "imagefilesdiv";
11884            fileLoadDiv.style.display = "none";
11885            fileLoadDiv.appendChild(fileLoadInput);
11886
11887            // node
11888            var node = app.getElement("loaderlist");
11889            // append
11890            node.appendChild(fileLoadDiv);
11891            // refresh
11892            dwv.gui.refreshElement(node);
11893        };
11894
11895        /**
11896         * Display the file load HTML.
11897         * @param {Boolean} bool True to display, false to hide.
11898         */
11899        this.display = function(bool) {
11900            // file div element
11901            var node = app.getElement("loaderlist");
11902            var filediv = node.getElementsByClassName("imagefilesdiv")[0];
11903            filediv.style.display = bool ? "" : "none";
11904        };
11905
11906    }; // class dwv.gui.base.FileLoad
11907
11908    /**
11909     * FolderLoad base gui.
11910     * @constructor
11911     */
11912    dwv.gui.base.FolderLoad = function(app) {
11913        // closure to self
11914        var self = this;
11915
11916        /**
11917         * Internal file input change handler.
11918         * @param {Object} event The change event.
11919         */
11920        function onchangeinternal(event) {
11921            if (typeof self.onchange === "function") {
11922                self.onchange(event);
11923            }
11924            app.onChangeFiles(event);
11925        }
11926
11927        /**
11928         * Setup the file load HTML to the page.
11929         */
11930        this.setup = function() {
11931            // input
11932            var fileLoadInput = document.createElement("input");
11933            fileLoadInput.onchange = onchangeinternal;
11934            fileLoadInput.type = "file";
11935            fileLoadInput.multiple = true;
11936            fileLoadInput.webkitdirectory = true;
11937            fileLoadInput.className = "imagefolder";
11938            fileLoadInput.setAttribute("data-clear-btn", "true");
11939            fileLoadInput.setAttribute("data-mini", "true");
11940
11941            // associated div
11942            var folderLoadDiv = document.createElement("div");
11943            folderLoadDiv.className = "imagefolderdiv";
11944            folderLoadDiv.style.display = "none";
11945            folderLoadDiv.appendChild(fileLoadInput);
11946
11947            // node
11948            var node = app.getElement("loaderlist");
11949            // append
11950            node.appendChild(folderLoadDiv);
11951            // refresh
11952            dwv.gui.refreshElement(node);
11953        };
11954
11955        /**
11956         * Display the folder load HTML.
11957         * @param {Boolean} bool True to display, false to hide.
11958         */
11959        this.display = function(bool) {
11960            // file div element
11961            var node = app.getElement("loaderlist");
11962            var folderdiv = node.getElementsByClassName("imagefolderdiv")[0];
11963            folderdiv.style.display = bool ? "" : "none";
11964        };
11965
11966    }; // class dwv.gui.base.FileLoad
11967
11968    /**
11969     * UrlLoad base gui.
11970     * @constructor
11971     */
11972    dwv.gui.base.UrlLoad = function(app) {
11973        // closure to self
11974        var self = this;
11975
11976        /**
11977         * Internal url input change handler.
11978         * @param {Object} event The change event.
11979         */
11980        function onchangeinternal(event) {
11981            if (typeof self.onchange === "function") {
11982                self.onchange(event);
11983            }
11984            app.onChangeURL(event);
11985        }
11986
11987        /**
11988         * Setup the url load HTML to the page.
11989         */
11990        this.setup = function() {
11991            // input
11992            var urlLoadInput = document.createElement("input");
11993            urlLoadInput.onchange = onchangeinternal;
11994            urlLoadInput.type = "url";
11995            urlLoadInput.className = "imageurl";
11996            urlLoadInput.setAttribute("data-clear-btn", "true");
11997            urlLoadInput.setAttribute("data-mini", "true");
11998
11999            // associated div
12000            var urlLoadDiv = document.createElement("div");
12001            urlLoadDiv.className = "imageurldiv";
12002            urlLoadDiv.style.display = "none";
12003            urlLoadDiv.appendChild(urlLoadInput);
12004
12005            // node
12006            var node = app.getElement("loaderlist");
12007            // append
12008            node.appendChild(urlLoadDiv);
12009            // refresh
12010            dwv.gui.refreshElement(node);
12011        };
12012
12013        /**
12014         * Display the url load HTML.
12015         * @param {Boolean} bool True to display, false to hide.
12016         */
12017        this.display = function(bool) {
12018            // url div element
12019            var node = app.getElement("loaderlist");
12020            var urldiv = node.getElementsByClassName("imageurldiv")[0];
12021            urldiv.style.display = bool ? "" : "none";
12022        };
12023
12024    }; // class dwv.gui.base.UrlLoad
12025
12026    // namespaces
12027    var dwv = dwv || {};
12028    dwv.html = dwv.html || {};
12029
12030    /**
12031     * Style class.
12032     * @constructor
12033     */
12034    dwv.html.Style = function() {
12035        /**
12036         * Font size.
12037         * @private
12038         * @type Number
12039         */
12040        var fontSize = 12;
12041        /**
12042         * Font family.
12043         * @private
12044         * @type String
12045         */
12046        var fontFamily = "Verdana";
12047        /**
12048         * Text colour.
12049         * @private
12050         * @type String
12051         */
12052        var textColour = "#fff";
12053        /**
12054         * Line colour.
12055         * @private
12056         * @type String
12057         */
12058        var lineColour = "#ffff80";
12059        /**
12060         * Display scale.
12061         * @private
12062         * @type Number
12063         */
12064        var displayScale = 1;
12065        /**
12066         * Stroke width.
12067         * @private
12068         * @type Number
12069         */
12070        var strokeWidth = 2;
12071
12072        /**
12073         * Get the font family.
12074         * @return {String} The font family.
12075         */
12076        this.getFontFamily = function() { return fontFamily; };
12077
12078        /**
12079         * Get the font size.
12080         * @return {Number} The font size.
12081         */
12082        this.getFontSize = function() { return fontSize; };
12083
12084        /**
12085         * Get the stroke width.
12086         * @return {Number} The stroke width.
12087         */
12088        this.getStrokeWidth = function() { return strokeWidth; };
12089
12090        /**
12091         * Get the text colour.
12092         * @return {String} The text colour.
12093         */
12094        this.getTextColour = function() { return textColour; };
12095
12096        /**
12097         * Get the line colour.
12098         * @return {String} The line colour.
12099         */
12100        this.getLineColour = function() { return lineColour; };
12101
12102        /**
12103         * Set the line colour.
12104         * @param {String} colour The line colour.
12105         */
12106        this.setLineColour = function(colour) { lineColour = colour; };
12107
12108        /**
12109         * Set the display scale.
12110         * @param {String} scale The display scale.
12111         */
12112        this.setScale = function(scale) { displayScale = scale; };
12113
12114        /**
12115         * Scale an input value.
12116         * @param {Number} value The value to scale.
12117         */
12118        this.scale = function(value) { return value / displayScale; };
12119    };
12120
12121    /**
12122     * Get the font definition string.
12123     * @return {String} The font definition string.
12124     */
12125    dwv.html.Style.prototype.getFontStr = function() {
12126        return ("normal " + this.getFontSize() + "px sans-serif");
12127    };
12128
12129    /**
12130     * Get the line height.
12131     * @return {Number} The line height.
12132     */
12133    dwv.html.Style.prototype.getLineHeight = function() {
12134        return (this.getFontSize() + this.getFontSize() / 5);
12135    };
12136
12137    /**
12138     * Get the font size scaled to the display.
12139     * @return {Number} The scaled font size.
12140     */
12141    dwv.html.Style.prototype.getScaledFontSize = function() {
12142        return this.scale(this.getFontSize());
12143    };
12144
12145    /**
12146     * Get the stroke width scaled to the display.
12147     * @return {Number} The scaled stroke width.
12148     */
12149    dwv.html.Style.prototype.getScaledStrokeWidth = function() {
12150        return this.scale(this.getStrokeWidth());
12151    };
12152
12153    // namespaces
12154    var dwv = dwv || {};
12155    dwv.gui = dwv.gui || {};
12156    dwv.gui.base = dwv.gui.base || {};
12157
12158    /**
12159     * Toolbox base gui.
12160     * @constructor
12161     */
12162    dwv.gui.base.Toolbox = function(app) {
12163        /**
12164         * Setup the toolbox HTML.
12165         */
12166        this.setup = function(list) {
12167            // tool select
12168            var toolSelector = dwv.html.createHtmlSelect("toolSelect", list, "tool");
12169            toolSelector.onchange = app.onChangeTool;
12170
12171            // tool list element
12172            var toolLi = document.createElement("li");
12173            toolLi.className = "toolLi ui-block-a";
12174            toolLi.style.display = "none";
12175            toolLi.appendChild(toolSelector);
12176
12177            // tool ul
12178            var toolUl = document.createElement("ul");
12179            toolUl.appendChild(toolLi);
12180            toolUl.className = "ui-grid-b";
12181
12182            // node
12183            var node = app.getElement("toolList");
12184            // append
12185            node.appendChild(toolUl);
12186            // refresh
12187            dwv.gui.refreshElement(node);
12188        };
12189
12190        /**
12191         * Display the toolbox HTML.
12192         * @param {Boolean} bool True to display, false to hide.
12193         */
12194        this.display = function(bool) {
12195            // tool list element
12196            var node = app.getElement("toolLi");
12197            dwv.html.displayElement(node, bool);
12198        };
12199
12200        /**
12201         * Initialise the toolbox HTML.
12202         */
12203        this.initialise = function(displays) {
12204            // tool select: reset selected option
12205            var toolSelector = app.getElement("toolSelect");
12206
12207            // update list
12208            var options = toolSelector.options;
12209            var selectedIndex = -1;
12210            for (var i = 0; i < options.length; ++i) {
12211                if (!displays[i]) {
12212                    options[i].style.display = "none";
12213                } else {
12214                    if (selectedIndex === -1) {
12215                        selectedIndex = i;
12216                    }
12217                    options[i].style.display = "";
12218                }
12219            }
12220            toolSelector.selectedIndex = selectedIndex;
12221
12222            // refresh
12223            dwv.gui.refreshElement(toolSelector);
12224        };
12225
12226    }; // dwv.gui.base.Toolbox
12227
12228    /**
12229     * WindowLevel tool base gui.
12230     * @constructor
12231     */
12232    dwv.gui.base.WindowLevel = function(app) {
12233        /**
12234         * Setup the tool HTML.
12235         */
12236        this.setup = function() {
12237            // preset select
12238            var wlSelector = dwv.html.createHtmlSelect("presetSelect", []);
12239            wlSelector.onchange = app.onChangeWindowLevelPreset;
12240            // colour map select
12241            var cmSelector = dwv.html.createHtmlSelect("colourMapSelect", dwv.tool.colourMaps, "colourmap");
12242            cmSelector.onchange = app.onChangeColourMap;
12243
12244            // preset list element
12245            var wlLi = document.createElement("li");
12246            wlLi.className = "wlLi ui-block-b";
12247            //wlLi.className = "wlLi";
12248            wlLi.style.display = "none";
12249            wlLi.appendChild(wlSelector);
12250            // colour map list element
12251            var cmLi = document.createElement("li");
12252            cmLi.className = "cmLi ui-block-c";
12253            //cmLi.className = "cmLi";
12254            cmLi.style.display = "none";
12255            cmLi.appendChild(cmSelector);
12256
12257            // node
12258            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
12259            // append preset
12260            node.appendChild(wlLi);
12261            // append colour map
12262            node.appendChild(cmLi);
12263            // refresh
12264            dwv.gui.refreshElement(node);
12265        };
12266
12267        /**
12268         * Display the tool HTML.
12269         * @param {Boolean} bool True to display, false to hide.
12270         */
12271        this.display = function(bool) {
12272            // presets list element
12273            var node = app.getElement("wlLi");
12274            dwv.html.displayElement(node, bool);
12275            // colour map list element
12276            node = app.getElement("cmLi");
12277            dwv.html.displayElement(node, bool);
12278        };
12279
12280        /**
12281         * Initialise the tool HTML.
12282         */
12283        this.initialise = function() {
12284            // create new preset select
12285            var wlSelector = dwv.html.createHtmlSelect("presetSelect",
12286                app.getViewController().getWindowLevelPresetsNames(), "wl.presets", true);
12287            wlSelector.onchange = app.onChangeWindowLevelPreset;
12288            wlSelector.title = "Select w/l preset.";
12289
12290            // copy html list
12291            var wlLi = app.getElement("wlLi");
12292            // clear node
12293            dwv.html.cleanNode(wlLi);
12294            // add children
12295            wlLi.appendChild(wlSelector);
12296            // refresh
12297            dwv.gui.refreshElement(wlLi);
12298
12299            // colour map select
12300            var cmSelector = app.getElement("colourMapSelect");
12301            cmSelector.selectedIndex = 0;
12302            // special monochrome1 case
12303            if (app.getImage().getPhotometricInterpretation() === "MONOCHROME1") {
12304                cmSelector.selectedIndex = 1;
12305            }
12306            // refresh
12307            dwv.gui.refreshElement(cmSelector);
12308        };
12309
12310    }; // class dwv.gui.base.WindowLevel
12311
12312    /**
12313     * Draw tool base gui.
12314     * @constructor
12315     */
12316    dwv.gui.base.Draw = function(app) {
12317        // default colours
12318        var colours = [
12319            "Yellow", "Red", "White", "Green", "Blue", "Lime", "Fuchsia", "Black"
12320        ];
12321        /**
12322         * Get the default colour.
12323         */
12324        this.getDefaultColour = function() {
12325            if (dwv.browser.hasInputColor()) {
12326                return "#FFFF80";
12327            } else {
12328                return colours[0];
12329            }
12330        };
12331
12332        /**
12333         * Setup the tool HTML.
12334         */
12335        this.setup = function(shapeList) {
12336            // shape select
12337            var shapeSelector = dwv.html.createHtmlSelect("shapeSelect", shapeList, "shape");
12338            shapeSelector.onchange = app.onChangeShape;
12339            // colour select
12340            var colourSelector = null;
12341            if (dwv.browser.hasInputColor()) {
12342                colourSelector = document.createElement("input");
12343                colourSelector.className = "colourSelect";
12344                colourSelector.type = "color";
12345                colourSelector.value = "#FFFF80";
12346            } else {
12347                colourSelector = dwv.html.createHtmlSelect("colourSelect", colours, "colour");
12348            }
12349            colourSelector.onchange = app.onChangeLineColour;
12350
12351            // shape list element
12352            var shapeLi = document.createElement("li");
12353            shapeLi.className = "shapeLi ui-block-c";
12354            shapeLi.style.display = "none";
12355            shapeLi.appendChild(shapeSelector);
12356            //shapeLi.setAttribute("class","ui-block-c");
12357            // colour list element
12358            var colourLi = document.createElement("li");
12359            colourLi.className = "colourLi ui-block-b";
12360            colourLi.style.display = "none";
12361            colourLi.appendChild(colourSelector);
12362            //colourLi.setAttribute("class","ui-block-b");
12363
12364            // node
12365            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
12366            // apend shape
12367            node.appendChild(shapeLi);
12368            // append colour
12369            node.appendChild(colourLi);
12370            // refresh
12371            dwv.gui.refreshElement(node);
12372        };
12373
12374        /**
12375         * Display the tool HTML.
12376         * @param {Boolean} bool True to display, false to hide.
12377         */
12378        this.display = function(bool) {
12379            // colour list element
12380            var node = app.getElement("colourLi");
12381            dwv.html.displayElement(node, bool);
12382            // shape list element
12383            node = app.getElement("shapeLi");
12384            dwv.html.displayElement(node, bool);
12385        };
12386
12387        /**
12388         * Initialise the tool HTML.
12389         */
12390        this.initialise = function() {
12391            // shape select: reset selected option
12392            var shapeSelector = app.getElement("shapeSelect");
12393            shapeSelector.selectedIndex = 0;
12394            // refresh
12395            dwv.gui.refreshElement(shapeSelector);
12396
12397            // colour select: reset selected option
12398            var colourSelector = app.getElement("colourSelect");
12399            if (!dwv.browser.hasInputColor()) {
12400                colourSelector.selectedIndex = 0;
12401            }
12402            // refresh
12403            dwv.gui.refreshElement(colourSelector);
12404        };
12405
12406    }; // class dwv.gui.base.Draw
12407
12408    /**
12409     * Base gui for a tool with a colour setting.
12410     * @constructor
12411     */
12412    dwv.gui.base.ColourTool = function(app, prefix) {
12413        // default colours
12414        var colours = [
12415            "Yellow", "Red", "White", "Green", "Blue", "Lime", "Fuchsia", "Black"
12416        ];
12417        // colour selector class
12418        var colourSelectClassName = prefix + "ColourSelect";
12419        // colour selector class
12420        var colourLiClassName = prefix + "ColourLi";
12421
12422        /**
12423         * Get the default colour.
12424         */
12425        this.getDefaultColour = function() {
12426            if (dwv.browser.hasInputColor()) {
12427                return "#FFFF80";
12428            } else {
12429                return colours[0];
12430            }
12431        };
12432
12433        /**
12434         * Setup the tool HTML.
12435         */
12436        this.setup = function() {
12437            // colour select
12438            var colourSelector = null;
12439            if (dwv.browser.hasInputColor()) {
12440                colourSelector = document.createElement("input");
12441                colourSelector.className = colourSelectClassName;
12442                colourSelector.type = "color";
12443                colourSelector.value = "#FFFF80";
12444            } else {
12445                colourSelector = dwv.html.createHtmlSelect(colourSelectClassName, colours, "colour");
12446            }
12447            colourSelector.onchange = app.onChangeLineColour;
12448
12449            // colour list element
12450            var colourLi = document.createElement("li");
12451            colourLi.className = colourLiClassName + " ui-block-b";
12452            colourLi.style.display = "none";
12453            //colourLi.setAttribute("class","ui-block-b");
12454            colourLi.appendChild(colourSelector);
12455
12456            // node
12457            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
12458            // apend colour
12459            node.appendChild(colourLi);
12460            // refresh
12461            dwv.gui.refreshElement(node);
12462        };
12463
12464        /**
12465         * Display the tool HTML.
12466         * @param {Boolean} bool True to display, false to hide.
12467         */
12468        this.display = function(bool) {
12469            // colour list
12470            var node = app.getElement(colourLiClassName);
12471            dwv.html.displayElement(node, bool);
12472        };
12473
12474        /**
12475         * Initialise the tool HTML.
12476         */
12477        this.initialise = function() {
12478            var colourSelector = app.getElement(colourSelectClassName);
12479            if (!dwv.browser.hasInputColor()) {
12480                colourSelector.selectedIndex = 0;
12481            }
12482            dwv.gui.refreshElement(colourSelector);
12483        };
12484
12485    }; // class dwv.gui.base.ColourTool
12486
12487    /**
12488     * ZoomAndPan tool base gui.
12489     * @constructor
12490     */
12491    dwv.gui.base.ZoomAndPan = function(app) {
12492        /**
12493         * Setup the tool HTML.
12494         */
12495        this.setup = function() {
12496            // reset button
12497            var button = document.createElement("button");
12498            button.className = "zoomResetButton";
12499            button.name = "zoomResetButton";
12500            button.onclick = app.onZoomReset;
12501            button.setAttribute("style", "width:100%; margin-top:0.5em;");
12502            button.setAttribute("class", "ui-btn ui-btn-b");
12503            var text = document.createTextNode(dwv.i18n("basics.reset"));
12504            button.appendChild(text);
12505
12506            // list element
12507            var liElement = document.createElement("li");
12508            liElement.className = "zoomLi ui-block-c";
12509            liElement.style.display = "none";
12510            //liElement.setAttribute("class","ui-block-c");
12511            liElement.appendChild(button);
12512
12513            // node
12514            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
12515            // append element
12516            node.appendChild(liElement);
12517            // refresh
12518            dwv.gui.refreshElement(node);
12519        };
12520
12521        /**
12522         * Display the tool HTML.
12523         * @param {Boolean} bool True to display, false to hide.
12524         */
12525        this.display = function(bool) {
12526            // display list element
12527            var node = app.getElement("zoomLi");
12528            dwv.html.displayElement(node, bool);
12529        };
12530
12531    }; // class dwv.gui.base.ZoomAndPan
12532
12533    /**
12534     * Scroll tool base gui.
12535     * @constructor
12536     */
12537    dwv.gui.base.Scroll = function(app) {
12538        /**
12539         * Setup the tool HTML.
12540         */
12541        this.setup = function() {
12542            // list element
12543            var liElement = document.createElement("li");
12544            liElement.className = "scrollLi ui-block-c";
12545            liElement.style.display = "none";
12546
12547            // node
12548            var node = app.getElement("toolList").getElementsByTagName("ul")[0];
12549            // append element
12550            node.appendChild(liElement);
12551            // refresh
12552            dwv.gui.refreshElement(node);
12553        };
12554
12555        /**
12556         * Display the tool HTML.
12557         * @param {Boolean} bool True to display, false to hide.
12558         */
12559        this.display = function(bool) {
12560            // display list element
12561            var node = app.getElement("scrollLi");
12562            dwv.html.displayElement(node, bool);
12563        };
12564
12565    }; // class dwv.gui.base.Scroll
12566
12567    // namespaces
12568    var dwv = dwv || {};
12569    dwv.gui = dwv.gui || {};
12570    dwv.gui.base = dwv.gui.base || {};
12571
12572    /**
12573     * Undo base gui.
12574     * @constructor
12575     */
12576    dwv.gui.base.Undo = function(app) {
12577        /**
12578         * Setup the undo HTML.
12579         */
12580        this.setup = function() {
12581            var paragraph = document.createElement("p");
12582            paragraph.appendChild(document.createTextNode("History:"));
12583            paragraph.appendChild(document.createElement("br"));
12584
12585            var select = document.createElement("select");
12586            select.className = "history_list";
12587            select.name = "history_list";
12588            select.multiple = "multiple";
12589            paragraph.appendChild(select);
12590
12591            // node
12592            var node = app.getElement("history");
12593            // clear it
12594            while (node.hasChildNodes()) {
12595                node.removeChild(node.firstChild);
12596            }
12597            // append
12598            node.appendChild(paragraph);
12599            // refresh
12600            dwv.gui.refreshElement(node);
12601        };
12602
12603        /**
12604         * Clear the command list of the undo HTML.
12605         */
12606        this.initialise = function() {
12607            var select = app.getElement("history_list");
12608            if (select && select.length !== 0) {
12609                for (var i = select.length - 1; i >= 0; --i) {
12610                    select.remove(i);
12611                }
12612            }
12613            // refresh
12614            dwv.gui.refreshElement(select);
12615        };
12616
12617        /**
12618         * Add a command to the undo HTML.
12619         * @param {String} commandName The name of the command to add.
12620         */
12621        this.addCommandToUndoHtml = function(commandName) {
12622            var select = app.getElement("history_list");
12623            // remove undone commands
12624            var count = select.length - (select.selectedIndex + 1);
12625            if (count > 0) {
12626                for (var i = 0; i < count; ++i) {
12627                    select.remove(select.length - 1);
12628                }
12629            }
12630            // add new option
12631            var option = document.createElement("option");
12632            option.text = commandName;
12633            option.value = commandName;
12634            select.add(option);
12635            // increment selected index
12636            select.selectedIndex++;
12637            // refresh
12638            dwv.gui.refreshElement(select);
12639        };
12640
12641        /**
12642         * Enable the last command of the undo HTML.
12643         * @param {Boolean} enable Flag to enable or disable the command.
12644         */
12645        this.enableInUndoHtml = function(enable) {
12646            var select = app.getElement("history_list");
12647            // enable or not (order is important)
12648            var option;
12649            if (enable) {
12650                // increment selected index
12651                select.selectedIndex++;
12652                // enable option
12653                option = select.options[select.selectedIndex];
12654                option.disabled = false;
12655            } else {
12656                // disable option
12657                option = select.options[select.selectedIndex];
12658                option.disabled = true;
12659                // decrement selected index
12660                select.selectedIndex--;
12661            }
12662            // refresh
12663            dwv.gui.refreshElement(select);
12664        };
12665
12666    }; // class dwv.gui.base.Undo
12667
12668    // namespaces
12669    var dwv = dwv || {};
12670    dwv.image = dwv.image || {};
12671
12672    // JPEG Baseline
12673    var hasJpegBaselineDecoder = (typeof JpegImage !== "undefined");
12674    var JpegImage = JpegImage || {};
12675    // JPEG Lossless
12676    var hasJpegLosslessDecoder = (typeof jpeg !== "undefined") &&
12677        (typeof jpeg.lossless !== "undefined");
12678    var jpeg = jpeg || {};
12679    jpeg.lossless = jpeg.lossless || {};
12680    // JPEG 2000
12681    var hasJpeg2000Decoder = (typeof JpxImage !== "undefined");
12682    var JpxImage = JpxImage || {};
12683
12684    /**
12685     * Asynchronous pixel buffer decoder.
12686     * @param {String} script The path to the decoder script to be used by the web worker.
12687     */
12688    dwv.image.AsynchPixelBufferDecoder = function(script) {
12689        // initialise the thread pool
12690        var pool = new dwv.utils.ThreadPool(15);
12691        pool.init();
12692
12693        /**
12694         * Decode a pixel buffer.
12695         * @param {Array} pixelBuffer The pixel buffer.
12696         * @param {Number} bitsAllocated The bits allocated per element in the buffer.
12697         * @param {Boolean} isSigned Is the data signed.
12698         * @param {Function} callback Callback function to handle decoded data.
12699         */
12700        this.decode = function(pixelBuffer, bitsAllocated, isSigned, callback) {
12701            // (re)set event handler
12702            pool.onpoolworkend = this.ondecodeend;
12703            pool.onworkerend = this.ondecoded;
12704            // create worker task
12705            var workerTask = new dwv.utils.WorkerTask(script, callback, {
12706                'buffer': pixelBuffer,
12707                'bitsAllocated': bitsAllocated,
12708                'isSigned': isSigned
12709            });
12710            // add it the queue and run it
12711            pool.addWorkerTask(workerTask);
12712        };
12713
12714        /**
12715         * Abort decoding.
12716         */
12717        this.abort = function() {
12718            // abort the thread pool
12719            pool.abort();
12720        };
12721    };
12722
12723    /**
12724     * Handle a decode end event.
12725     */
12726    dwv.image.AsynchPixelBufferDecoder.prototype.ondecodeend = function() {
12727        // default does nothing.
12728    };
12729
12730    /**
12731     * Handle a decode event.
12732     */
12733    dwv.image.AsynchPixelBufferDecoder.prototype.ondecoded = function() {
12734        // default does nothing.
12735    };
12736
12737    /**
12738     * Synchronous pixel buffer decoder.
12739     * @param {String} algoName The decompression algorithm name.
12740     */
12741    dwv.image.SynchPixelBufferDecoder = function(algoName) {
12742        /**
12743         * Decode a pixel buffer.
12744         * @param {Array} pixelBuffer The pixel buffer.
12745         * @param {Number} bitsAllocated The bits allocated per element in the buffer.
12746         * @param {Boolean} isSigned Is the data signed.
12747         * @param {Function} callback Callback function to handle decoded data.
12748         * @external jpeg
12749         * @external JpegImage
12750         * @external JpxImage
12751         */
12752        this.decode = function(pixelBuffer, bitsAllocated, isSigned, callback) {
12753            var decoder = null;
12754            var decodedBuffer = null;
12755            if (algoName === "jpeg-lossless") {
12756                if (!hasJpegLosslessDecoder) {
12757                    throw new Error("No JPEG Lossless decoder provided");
12758                }
12759                // bytes per element
12760                var bpe = bitsAllocated / 8;
12761                var buf = new Uint8Array(pixelBuffer);
12762                decoder = new jpeg.lossless.Decoder();
12763                var decoded = decoder.decode(buf.buffer, 0, buf.buffer.byteLength, bpe);
12764                if (bitsAllocated === 8) {
12765                    if (isSigned) {
12766                        decodedBuffer = new Int8Array(decoded.buffer);
12767                    } else {
12768                        decodedBuffer = new Uint8Array(decoded.buffer);
12769                    }
12770                } else if (bitsAllocated === 16) {
12771                    if (isSigned) {
12772                        decodedBuffer = new Int16Array(decoded.buffer);
12773                    } else {
12774                        decodedBuffer = new Uint16Array(decoded.buffer);
12775                    }
12776                }
12777            } else if (algoName === "jpeg-baseline") {
12778                if (!hasJpegBaselineDecoder) {
12779                    throw new Error("No JPEG Baseline decoder provided");
12780                }
12781                decoder = new JpegImage();
12782                decoder.parse(pixelBuffer);
12783                decodedBuffer = decoder.getData(decoder.width, decoder.height);
12784            } else if (algoName === "jpeg2000") {
12785                if (!hasJpeg2000Decoder) {
12786                    throw new Error("No JPEG 2000 decoder provided");
12787                }
12788                // decompress pixel buffer into Int16 image
12789                decoder = new JpxImage();
12790                decoder.parse(pixelBuffer);
12791                // set the pixel buffer
12792                decodedBuffer = decoder.tiles[0].items;
12793            }
12794            // send events
12795            this.ondecoded();
12796            this.ondecodeend();
12797            // call callback with decoded buffer as array
12798            callback({ data: [decodedBuffer] });
12799        };
12800
12801        /**
12802         * Abort decoding.
12803         */
12804        this.abort = function() {
12805            // nothing to do in the synchronous case.
12806        };
12807    };
12808
12809    /**
12810     * Handle a decode end event.
12811     */
12812    dwv.image.SynchPixelBufferDecoder.prototype.ondecodeend = function() {
12813        // default does nothing.
12814    };
12815
12816    /**
12817     * Handle a decode event.
12818     */
12819    dwv.image.SynchPixelBufferDecoder.prototype.ondecoded = function() {
12820        // default does nothing.
12821    };
12822
12823    /**
12824     * Decode a pixel buffer.
12825     * @constructor
12826     * @param {String} algoName The decompression algorithm name.
12827     * If the 'dwv.image.decoderScripts' variable does not contain the desired algorythm,
12828     * the decoder will switch to the synchronous mode.
12829     */
12830    dwv.image.PixelBufferDecoder = function(algoName) {
12831        /**
12832         * Pixel decoder.
12833         * Defined only once.
12834         * @private
12835         * @type Object
12836         */
12837        var pixelDecoder = null;
12838
12839        // initialise the asynch decoder (if possible)
12840        if (typeof dwv.image.decoderScripts !== "undefined" &&
12841            typeof dwv.image.decoderScripts[algoName] !== "undefined") {
12842            pixelDecoder = new dwv.image.AsynchPixelBufferDecoder(dwv.image.decoderScripts[algoName]);
12843        } else {
12844            pixelDecoder = new dwv.image.SynchPixelBufferDecoder(algoName);
12845        }
12846
12847        /**
12848         * Get data from an input buffer using a DICOM parser.
12849         * @param {Array} pixelBuffer The input data buffer.
12850         * @param {Number} bitsAllocated The bits allocated per element in the buffer.
12851         * @param {Boolean} isSigned Is the data signed.
12852         * @param {Object} callback The callback on the conversion.
12853         */
12854        this.decode = function(pixelBuffer, bitsAllocated, isSigned, callback) {
12855            // set event handler
12856            pixelDecoder.ondecodeend = this.ondecodeend;
12857            pixelDecoder.ondecoded = this.ondecoded;
12858            // decode and call the callback
12859            pixelDecoder.decode(pixelBuffer, bitsAllocated, isSigned, callback);
12860        };
12861
12862        /**
12863         * Abort decoding.
12864         */
12865        this.abort = function() {
12866            // decoder classes should define an abort
12867            pixelDecoder.abort();
12868        };
12869    };
12870
12871    /**
12872     * Handle a decode end event.
12873     */
12874    dwv.image.PixelBufferDecoder.prototype.ondecodeend = function() {
12875        // default does nothing.
12876    };
12877
12878    /**
12879     * Handle a decode end event.
12880     */
12881    dwv.image.PixelBufferDecoder.prototype.ondecoded = function() {
12882        // default does nothing.
12883    };
12884
12885    // namespaces
12886    var dwv = dwv || {};
12887    dwv.image = dwv.image || {};
12888
12889    /**
12890     * Create a dwv.image.View from a DICOM buffer.
12891     * @constructor
12892     */
12893    dwv.image.DicomBufferToView = function() {
12894        // closure to self
12895        var self = this;
12896
12897        /**
12898         * The default character set (optional).
12899         * @private
12900         * @type String
12901         */
12902        var defaultCharacterSet;
12903
12904        /**
12905         * Set the default character set.
12906         * param {String} The character set.
12907         */
12908        this.setDefaultCharacterSet = function(characterSet) {
12909            defaultCharacterSet = characterSet;
12910        };
12911
12912        /**
12913         * Pixel buffer decoder.
12914         * Define only once to allow optional asynchronous mode.
12915         * @private
12916         * @type Object
12917         */
12918        var pixelDecoder = null;
12919
12920        /**
12921         * Get data from an input buffer using a DICOM parser.
12922         * @param {Array} buffer The input data buffer.
12923         * @param {Number} dataIndex The data index.
12924         */
12925        this.convert = function(buffer, dataIndex) {
12926            // DICOM parser
12927            var dicomParser = new dwv.dicom.DicomParser();
12928            dicomParser.setDefaultCharacterSet(defaultCharacterSet);
12929            // parse the buffer
12930            dicomParser.parse(buffer);
12931
12932            var pixelBuffer = dicomParser.getRawDicomElements().x7FE00010.value;
12933            var syntax = dwv.dicom.cleanString(dicomParser.getRawDicomElements().x00020010.value[0]);
12934            var algoName = dwv.dicom.getSyntaxDecompressionName(syntax);
12935            var needDecompression = (algoName !== null);
12936
12937            // worker callback
12938            var onDecodedFirstFrame = function( /*event*/ ) {
12939                // create the image
12940                var imageFactory = new dwv.image.ImageFactory();
12941                var image = imageFactory.create(dicomParser.getDicomElements(), pixelBuffer);
12942                // create the view
12943                var viewFactory = new dwv.image.ViewFactory();
12944                var view = viewFactory.create(dicomParser.getDicomElements(), image);
12945                // return
12946                self.onload({ "view": view, "info": dicomParser.getDicomElements().dumpToTable() });
12947            };
12948
12949            if (needDecompression) {
12950                var bitsAllocated = dicomParser.getRawDicomElements().x00280100.value[0];
12951                var pixelRepresentation = dicomParser.getRawDicomElements().x00280103.value[0];
12952                var isSigned = (pixelRepresentation === 1);
12953                var nFrames = pixelBuffer.length;
12954
12955                if (!pixelDecoder) {
12956                    pixelDecoder = new dwv.image.PixelBufferDecoder(algoName);
12957                }
12958
12959                // loadend event
12960                pixelDecoder.ondecodeend = function() {
12961                    self.onloadend();
12962                };
12963
12964                // send an onload event for mono frame
12965                if (nFrames === 1) {
12966                    pixelDecoder.ondecoded = function() {
12967                        self.onloadend();
12968                    };
12969                }
12970
12971                // decoder callback
12972                var countDecodedFrames = 0;
12973                var onDecodedFrame = function(frame) {
12974                    return function(event) {
12975                        // send progress
12976                        ++countDecodedFrames;
12977                        var ev = {
12978                            'type': "load-progress",
12979                            'lengthComputable': true,
12980                            'loaded': (countDecodedFrames * 100 / nFrames),
12981                            'total': 100
12982                        };
12983                        if (typeof dataIndex !== "undefined") {
12984                            ev.index = dataIndex;
12985                        }
12986                        self.onprogress(ev);
12987                        // store data
12988                        pixelBuffer[frame] = event.data[0];
12989                        // create image for first frame
12990                        if (frame === 0) {
12991                            onDecodedFirstFrame();
12992                        }
12993                    };
12994                };
12995
12996                // decompress synchronously the first frame to create the image
12997                pixelDecoder.decode(pixelBuffer[0],
12998                    bitsAllocated, isSigned, onDecodedFrame(0), false);
12999
13000                // decompress the possible other frames
13001                if (nFrames !== 1) {
13002                    // decode (asynchronously if possible)
13003                    for (var f = 1; f < nFrames; ++f) {
13004                        pixelDecoder.decode(pixelBuffer[f],
13005                            bitsAllocated, isSigned, onDecodedFrame(f));
13006                    }
13007                }
13008            }
13009            // no decompression
13010            else {
13011                // send progress
13012                var evnodec = {
13013                    'type': 'load-progress',
13014                    'lengthComputable': true,
13015                    'loaded': 100,
13016                    'total': 100
13017                };
13018                if (typeof dataIndex !== "undefined") {
13019                    evnodec.index = dataIndex;
13020                }
13021                self.onprogress(evnodec);
13022                // create image
13023                onDecodedFirstFrame();
13024                // send load events
13025                self.onloadend();
13026            }
13027        };
13028
13029        /**
13030         * Abort a conversion.
13031         */
13032        this.abort = function() {
13033            if (pixelDecoder) {
13034                pixelDecoder.abort();
13035            }
13036        };
13037    };
13038
13039    /**
13040     * Handle a load end event.
13041     * @param {Object} event The load end event.
13042     * Default does nothing.
13043     */
13044    dwv.image.DicomBufferToView.prototype.onloadend = function( /*event*/ ) {};
13045    /**
13046     * Handle a load event.
13047     * @param {Object} event The load event.
13048     * Default does nothing.
13049     */
13050    dwv.image.DicomBufferToView.prototype.onload = function( /*event*/ ) {};
13051    /**
13052     * Handle a load progress event.
13053     * @param {Object} event The progress event.
13054     * Default does nothing.
13055     */
13056    dwv.image.DicomBufferToView.prototype.onprogress = function( /*event*/ ) {};
13057
13058    // namespaces
13059    var dwv = dwv || {};
13060    dwv.image = dwv.image || {};
13061
13062    /**
13063     * Create a simple array buffer from an ImageData buffer.
13064     * @param {Object} imageData The ImageData taken from a context.
13065     * @return {Array} The image buffer.
13066     */
13067    dwv.image.imageDataToBuffer = function(imageData) {
13068        // remove alpha
13069        // TODO support passing the full image data
13070        var dataLen = imageData.data.length;
13071        var buffer = new Uint8Array((dataLen / 4) * 3);
13072        var j = 0;
13073        for (var i = 0; i < dataLen; i += 4) {
13074            buffer[j] = imageData.data[i];
13075            buffer[j + 1] = imageData.data[i + 1];
13076            buffer[j + 2] = imageData.data[i + 2];
13077            j += 3;
13078        }
13079        return buffer;
13080    };
13081
13082    /**
13083     * Get data from an input context imageData.
13084     * @param {Number} width The width of the coresponding image.
13085     * @param {Number} height The height of the coresponding image.
13086     * @param {Number} sliceIndex The slice index of the imageData.
13087     * @param {Object} imageBuffer The image buffer.
13088     * @param {Number} numberOfFrames The final number of frames.
13089     * @return {Object} The corresponding view.
13090     */
13091    dwv.image.getDefaultView = function(
13092        width, height, sliceIndex,
13093        imageBuffer, numberOfFrames, info) {
13094        // image size
13095        var imageSize = new dwv.image.Size(width, height);
13096        // default spacing
13097        // TODO: misleading...
13098        var imageSpacing = new dwv.image.Spacing(1, 1);
13099        // default origin
13100        var origin = new dwv.math.Point3D(0, 0, sliceIndex);
13101        // create image
13102        var geometry = new dwv.image.Geometry(origin, imageSize, imageSpacing);
13103        var image = new dwv.image.Image(geometry, imageBuffer, numberOfFrames);
13104        image.setPhotometricInterpretation("RGB");
13105        // meta information
13106        var meta = {};
13107        meta.BitsStored = 8;
13108        image.setMeta(meta);
13109        // overlay
13110        image.setFirstOverlay(dwv.gui.info.createOverlaysForDom(info));
13111        // view
13112        var view = new dwv.image.View(image);
13113        // defaut preset
13114        view.setWindowLevelMinMax();
13115        // return
13116        return view;
13117    };
13118
13119    /**
13120     * Get data from an input image using a canvas.
13121     * @param {Object} image The DOM Image.
13122     * @return {Mixed} The corresponding view and info.
13123     */
13124    dwv.image.getViewFromDOMImage = function(image) {
13125        // image size
13126        var width = image.width;
13127        var height = image.height;
13128
13129        // draw the image in the canvas in order to get its data
13130        var canvas = document.createElement('canvas');
13131        canvas.width = width;
13132        canvas.height = height;
13133        var ctx = canvas.getContext('2d');
13134        ctx.drawImage(image, 0, 0);
13135        // get the image data
13136        var imageData = ctx.getImageData(0, 0, width, height);
13137
13138        // image properties
13139        var info = [];
13140        if (typeof image.origin === "string") {
13141            info.push({ "name": "origin", "value": image.origin });
13142        } else {
13143            info.push({ "name": "fileName", "value": image.origin.name });
13144            info.push({ "name": "fileType", "value": image.origin.type });
13145            info.push({ "name": "fileLastModifiedDate", "value": image.origin.lastModifiedDate });
13146        }
13147        info.push({ "name": "imageWidth", "value": width });
13148        info.push({ "name": "imageHeight", "value": height });
13149
13150        // create view
13151        var sliceIndex = image.index ? image.index : 0;
13152        var imageBuffer = dwv.image.imageDataToBuffer(imageData);
13153        var view = dwv.image.getDefaultView(
13154            width, height, sliceIndex, [imageBuffer], 1, info);
13155
13156        // return
13157        return { "view": view, "info": info };
13158    };
13159
13160    /**
13161     * Get data from an input image using a canvas.
13162     * @param {Object} video The DOM Video.
13163     * @param {Object} callback The function to call once the data is loaded.
13164     * @param {Object} cbprogress The function to call to report progress.
13165     * @param {Object} cbonloadend The function to call to report load end.
13166     * @param {Number} dataindex The data index.
13167     */
13168    dwv.image.getViewFromDOMVideo = function(video, callback, cbprogress, cbonloadend, dataIndex) {
13169        // video size
13170        var width = video.videoWidth;
13171        var height = video.videoHeight;
13172
13173        // default frame rate...
13174        var frameRate = 30;
13175        // number of frames
13176        var numberOfFrames = Math.floor(video.duration * frameRate);
13177
13178        // video properties
13179        var info = [];
13180        if (video.file) {
13181            info.push({ "name": "fileName", "value": video.file.name });
13182            info.push({ "name": "fileType", "value": video.file.type });
13183            info.push({ "name": "fileLastModifiedDate", "value": video.file.lastModifiedDate });
13184        }
13185        info.push({ "name": "imageWidth", "value": width });
13186        info.push({ "name": "imageHeight", "value": height });
13187        info.push({ "name": "numberOfFrames", "value": numberOfFrames });
13188
13189        // draw the image in the canvas in order to get its data
13190        var canvas = document.createElement('canvas');
13191        canvas.width = width;
13192        canvas.height = height;
13193        var ctx = canvas.getContext('2d');
13194
13195        // using seeked to loop through all video frames
13196        video.addEventListener('seeked', onseeked, false);
13197
13198        // current frame index
13199        var frameIndex = 0;
13200        // video view
13201        var view = null;
13202
13203        // draw the context and store it as a frame
13204        function storeFrame() {
13205            // send progress
13206            var evprog = {
13207                'type': 'load-progress',
13208                'lengthComputable': true,
13209                'loaded': frameIndex,
13210                'total': numberOfFrames
13211            };
13212            if (typeof dataIndex !== "undefined") {
13213                evprog.index = dataIndex;
13214            }
13215            cbprogress(evprog);
13216            // draw image
13217            ctx.drawImage(video, 0, 0);
13218            // context to image buffer
13219            var imgBuffer = dwv.image.imageDataToBuffer(
13220                ctx.getImageData(0, 0, width, height));
13221            if (frameIndex === 0) {
13222                // create view
13223                view = dwv.image.getDefaultView(
13224                    width, height, 1, [imgBuffer], numberOfFrames, info);
13225                // call callback
13226                callback({ "view": view, "info": info });
13227            } else {
13228                view.appendFrameBuffer(imgBuffer);
13229            }
13230        }
13231
13232        // handle seeked event
13233        function onseeked( /*event*/ ) {
13234            // store
13235            storeFrame();
13236            // increment index
13237            ++frameIndex;
13238            // set the next time
13239            // (not using currentTime, it seems to get offseted)
13240            var nextTime = frameIndex / frameRate;
13241            if (nextTime <= this.duration) {
13242                this.currentTime = nextTime;
13243            } else {
13244                cbonloadend();
13245                // stop listening
13246                video.removeEventListener('seeked', onseeked);
13247            }
13248        }
13249
13250        // trigger the first seeked
13251        video.currentTime = 0;
13252    };
13253
13254    // namespaces
13255    var dwv = dwv || {};
13256    dwv.image = dwv.image || {};
13257    /** @namespace */
13258    dwv.image.filter = dwv.image.filter || {};
13259
13260    /**
13261     * Threshold an image between an input minimum and maximum.
13262     * @constructor
13263     */
13264    dwv.image.filter.Threshold = function() {
13265        /**
13266         * Threshold minimum.
13267         * @private
13268         * @type Number
13269         */
13270        var min = 0;
13271        /**
13272         * Threshold maximum.
13273         * @private
13274         * @type Number
13275         */
13276        var max = 0;
13277
13278        /**
13279         * Get the threshold minimum.
13280         * @return {Number} The threshold minimum.
13281         */
13282        this.getMin = function() { return min; };
13283        /**
13284         * Set the threshold minimum.
13285         * @param {Number} val The threshold minimum.
13286         */
13287        this.setMin = function(val) { min = val; };
13288        /**
13289         * Get the threshold maximum.
13290         * @return {Number} The threshold maximum.
13291         */
13292        this.getMax = function() { return max; };
13293        /**
13294         * Set the threshold maximum.
13295         * @param {Number} val The threshold maximum.
13296         */
13297        this.setMax = function(val) { max = val; };
13298        /**
13299         * Get the name of the filter.
13300         * @return {String} The name of the filter.
13301         */
13302        this.getName = function() { return "Threshold"; };
13303
13304        /**
13305         * Original image.
13306         * @private
13307         * @type Object
13308         */
13309        var originalImage = null;
13310        /**
13311         * Set the original image.
13312         * @param {Object} image The original image.
13313         */
13314        this.setOriginalImage = function(image) { originalImage = image; };
13315        /**
13316         * Get the original image.
13317         * @return {Object} image The original image.
13318         */
13319        this.getOriginalImage = function() { return originalImage; };
13320    };
13321
13322    /**
13323     * Transform the main image using this filter.
13324     * @return {Object} The transformed image.
13325     */
13326    dwv.image.filter.Threshold.prototype.update = function() {
13327        var image = this.getOriginalImage();
13328        var imageMin = image.getDataRange().min;
13329        var self = this;
13330        var threshFunction = function(value) {
13331            if (value < self.getMin() || value > self.getMax()) {
13332                return imageMin;
13333            } else {
13334                return value;
13335            }
13336        };
13337        return image.transform(threshFunction);
13338    };
13339
13340    /**
13341     * Sharpen an image using a sharpen convolution matrix.
13342     * @constructor
13343     */
13344    dwv.image.filter.Sharpen = function() {
13345        /**
13346         * Get the name of the filter.
13347         * @return {String} The name of the filter.
13348         */
13349        this.getName = function() { return "Sharpen"; };
13350        /**
13351         * Original image.
13352         * @private
13353         * @type Object
13354         */
13355        var originalImage = null;
13356        /**
13357         * Set the original image.
13358         * @param {Object} image The original image.
13359         */
13360        this.setOriginalImage = function(image) { originalImage = image; };
13361        /**
13362         * Get the original image.
13363         * @return {Object} image The original image.
13364         */
13365        this.getOriginalImage = function() { return originalImage; };
13366    };
13367
13368    /**
13369     * Transform the main image using this filter.
13370     * @return {Object} The transformed image.
13371     */
13372    dwv.image.filter.Sharpen.prototype.update = function() {
13373        var image = this.getOriginalImage();
13374
13375        return image.convolute2D(
13376            [0, -1, 0, -1, 5, -1,
13377                0, -1, 0
13378            ]);
13379    };
13380
13381    /**
13382     * Apply a Sobel filter to an image.
13383     * @constructor
13384     */
13385    dwv.image.filter.Sobel = function() {
13386        /**
13387         * Get the name of the filter.
13388         * @return {String} The name of the filter.
13389         */
13390        this.getName = function() { return "Sobel"; };
13391        /**
13392         * Original image.
13393         * @private
13394         * @type Object
13395         */
13396        var originalImage = null;
13397        /**
13398         * Set the original image.
13399         * @param {Object} image The original image.
13400         */
13401        this.setOriginalImage = function(image) { originalImage = image; };
13402        /**
13403         * Get the original image.
13404         * @return {Object} image The original image.
13405         */
13406        this.getOriginalImage = function() { return originalImage; };
13407    };
13408
13409    /**
13410     * Transform the main image using this filter.
13411     * @return {Object} The transformed image.
13412     */
13413    dwv.image.filter.Sobel.prototype.update = function() {
13414        var image = this.getOriginalImage();
13415
13416        var gradX = image.convolute2D(
13417            [1, 0, -1,
13418                2, 0, -2,
13419                1, 0, -1
13420            ]);
13421
13422        var gradY = image.convolute2D(
13423            [1, 2, 1,
13424                0, 0, 0, -1, -2, -1
13425            ]);
13426
13427        return gradX.compose(gradY, function(x, y) { return Math.sqrt(x * x + y * y); });
13428    };
13429
13430    // namespaces
13431    var dwv = dwv || {};
13432    dwv.image = dwv.image || {};
13433
13434    /**
13435     * 2D/3D Size class.
13436     * @constructor
13437     * @param {Number} numberOfColumns The number of columns.
13438     * @param {Number} numberOfRows The number of rows.
13439     * @param {Number} numberOfSlices The number of slices.
13440     */
13441    dwv.image.Size = function(numberOfColumns, numberOfRows, numberOfSlices) {
13442        /**
13443         * Get the number of columns.
13444         * @return {Number} The number of columns.
13445         */
13446        this.getNumberOfColumns = function() { return numberOfColumns; };
13447        /**
13448         * Get the number of rows.
13449         * @return {Number} The number of rows.
13450         */
13451        this.getNumberOfRows = function() { return numberOfRows; };
13452        /**
13453         * Get the number of slices.
13454         * @return {Number} The number of slices.
13455         */
13456        this.getNumberOfSlices = function() { return (numberOfSlices || 1.0); };
13457    };
13458
13459    /**
13460     * Get the size of a slice.
13461     * @return {Number} The size of a slice.
13462     */
13463    dwv.image.Size.prototype.getSliceSize = function() {
13464        return this.getNumberOfColumns() * this.getNumberOfRows();
13465    };
13466
13467    /**
13468     * Get the total size.
13469     * @return {Number} The total size.
13470     */
13471    dwv.image.Size.prototype.getTotalSize = function() {
13472        return this.getSliceSize() * this.getNumberOfSlices();
13473    };
13474
13475    /**
13476     * Check for equality.
13477     * @param {Size} rhs The object to compare to.
13478     * @return {Boolean} True if both objects are equal.
13479     */
13480    dwv.image.Size.prototype.equals = function(rhs) {
13481        return rhs !== null &&
13482            this.getNumberOfColumns() === rhs.getNumberOfColumns() &&
13483            this.getNumberOfRows() === rhs.getNumberOfRows() &&
13484            this.getNumberOfSlices() === rhs.getNumberOfSlices();
13485    };
13486
13487    /**
13488     * Check that coordinates are within bounds.
13489     * @param {Number} i The column coordinate.
13490     * @param {Number} j The row coordinate.
13491     * @param {Number} k The slice coordinate.
13492     * @return {Boolean} True if the given coordinates are within bounds.
13493     */
13494    dwv.image.Size.prototype.isInBounds = function(i, j, k) {
13495        if (i < 0 || i > this.getNumberOfColumns() - 1 ||
13496            j < 0 || j > this.getNumberOfRows() - 1 ||
13497            k < 0 || k > this.getNumberOfSlices() - 1) {
13498            return false;
13499        }
13500        return true;
13501    };
13502
13503    /**
13504     * Get a string representation of the Vector3D.
13505     * @return {String} The vector as a string.
13506     */
13507    dwv.image.Size.prototype.toString = function() {
13508        return "(" + this.getNumberOfColumns() +
13509            ", " + this.getNumberOfRows() +
13510            ", " + this.getNumberOfSlices() + ")";
13511    };
13512
13513    /**
13514     * 2D/3D Spacing class.
13515     * @constructor
13516     * @param {Number} columnSpacing The column spacing.
13517     * @param {Number} rowSpacing The row spacing.
13518     * @param {Number} sliceSpacing The slice spacing.
13519     */
13520    dwv.image.Spacing = function(columnSpacing, rowSpacing, sliceSpacing) {
13521        /**
13522         * Get the column spacing.
13523         * @return {Number} The column spacing.
13524         */
13525        this.getColumnSpacing = function() { return columnSpacing; };
13526        /**
13527         * Get the row spacing.
13528         * @return {Number} The row spacing.
13529         */
13530        this.getRowSpacing = function() { return rowSpacing; };
13531        /**
13532         * Get the slice spacing.
13533         * @return {Number} The slice spacing.
13534         */
13535        this.getSliceSpacing = function() { return (sliceSpacing || 1.0); };
13536    };
13537
13538    /**
13539     * Check for equality.
13540     * @param {Spacing} rhs The object to compare to.
13541     * @return {Boolean} True if both objects are equal.
13542     */
13543    dwv.image.Spacing.prototype.equals = function(rhs) {
13544        return rhs !== null &&
13545            this.getColumnSpacing() === rhs.getColumnSpacing() &&
13546            this.getRowSpacing() === rhs.getRowSpacing() &&
13547            this.getSliceSpacing() === rhs.getSliceSpacing();
13548    };
13549
13550    /**
13551     * Get a string representation of the Vector3D.
13552     * @return {String} The vector as a string.
13553     */
13554    dwv.image.Spacing.prototype.toString = function() {
13555        return "(" + this.getColumnSpacing() +
13556            ", " + this.getRowSpacing() +
13557            ", " + this.getSliceSpacing() + ")";
13558    };
13559
13560
13561    /**
13562     * 2D/3D Geometry class.
13563     * @constructor
13564     * @param {Object} origin The object origin (a 3D point).
13565     * @param {Object} size The object size.
13566     * @param {Object} spacing The object spacing.
13567     * @param {Object} orientation The object orientation (3*3 matrix, default to 3*3 identity).
13568     */
13569    dwv.image.Geometry = function(origin, size, spacing, orientation) {
13570        // check input origin
13571        if (typeof origin === 'undefined') {
13572            origin = new dwv.math.Point3D(0, 0, 0);
13573        }
13574        var origins = [origin];
13575        // check input orientation
13576        if (typeof orientation === 'undefined') {
13577            orientation = new dwv.math.getIdentityMat33();
13578        }
13579
13580        /**
13581         * Get the object first origin.
13582         * @return {Object} The object first origin.
13583         */
13584        this.getOrigin = function() { return origin; };
13585        /**
13586         * Get the object origins.
13587         * @return {Array} The object origins.
13588         */
13589        this.getOrigins = function() { return origins; };
13590        /**
13591         * Get the object size.
13592         * @return {Object} The object size.
13593         */
13594        this.getSize = function() { return size; };
13595        /**
13596         * Get the object spacing.
13597         * @return {Object} The object spacing.
13598         */
13599        this.getSpacing = function() { return spacing; };
13600        /**
13601         * Get the object orientation.
13602         * @return {Object} The object orientation.
13603         */
13604        this.getOrientation = function() { return orientation; };
13605
13606        /**
13607         * Get the slice position of a point in the current slice layout.
13608         * @param {Object} point The point to evaluate.
13609         */
13610        this.getSliceIndex = function(point) {
13611            // cannot use this.worldToIndex(point).getK() since
13612            // we cannot guaranty consecutive slices...
13613
13614            // find the closest index
13615            var closestSliceIndex = 0;
13616            var minDist = point.getDistance(origins[0]);
13617            var dist = 0;
13618            for (var i = 0; i < origins.length; ++i) {
13619                dist = point.getDistance(origins[i]);
13620                if (dist < minDist) {
13621                    minDist = dist;
13622                    closestSliceIndex = i;
13623                }
13624            }
13625            // we have the closest point, are we before or after
13626            var normal = new dwv.math.Vector3D(
13627                orientation.get(2, 0), orientation.get(2, 1), orientation.get(2, 2));
13628            var dotProd = normal.dotProduct(point.minus(origins[closestSliceIndex]));
13629            var sliceIndex = (dotProd > 0) ? closestSliceIndex + 1 : closestSliceIndex;
13630            return sliceIndex;
13631        };
13632
13633        /**
13634         * Append an origin to the geometry.
13635         * @param {Object} origin The origin to append.
13636         * @param {Number} index The index at which to append.
13637         */
13638        this.appendOrigin = function(origin, index) {
13639            // add in origin array
13640            origins.splice(index, 0, origin);
13641            // increment slice number
13642            size = new dwv.image.Size(
13643                size.getNumberOfColumns(),
13644                size.getNumberOfRows(),
13645                size.getNumberOfSlices() + 1);
13646        };
13647
13648    };
13649
13650    /**
13651     * Check for equality.
13652     * @param {Geometry} rhs The object to compare to.
13653     * @return {Boolean} True if both objects are equal.
13654     */
13655    dwv.image.Geometry.prototype.equals = function(rhs) {
13656        return rhs !== null &&
13657            this.getOrigin() === rhs.getOrigin() &&
13658            this.getSize() === rhs.getSize() &&
13659            this.getSpacing() === rhs.getSpacing();
13660    };
13661
13662    /**
13663     * Convert an index to an offset in memory.
13664     * @param {Object} index The index to convert.
13665     */
13666    dwv.image.Geometry.prototype.indexToOffset = function(index) {
13667        var size = this.getSize();
13668        return index.getI() +
13669            index.getJ() * size.getNumberOfColumns() +
13670            index.getK() * size.getSliceSize();
13671    };
13672
13673    /**
13674     * Convert an index into world coordinates.
13675     * @param {Object} index The index to convert.
13676     */
13677    dwv.image.Geometry.prototype.indexToWorld = function(index) {
13678        var origin = this.getOrigin();
13679        var spacing = this.getSpacing();
13680        return new dwv.math.Point3D(
13681            origin.getX() + index.getI() * spacing.getColumnSpacing(),
13682            origin.getY() + index.getJ() * spacing.getRowSpacing(),
13683            origin.getZ() + index.getK() * spacing.getSliceSpacing());
13684    };
13685
13686    /**
13687     * Convert world coordinates into an index.
13688     * @param {Object} THe point to convert.
13689     */
13690    dwv.image.Geometry.prototype.worldToIndex = function(point) {
13691        var origin = this.getOrigin();
13692        var spacing = this.getSpacing();
13693        return new dwv.math.Point3D(
13694            point.getX() / spacing.getColumnSpacing() - origin.getX(),
13695            point.getY() / spacing.getRowSpacing() - origin.getY(),
13696            point.getZ() / spacing.getSliceSpacing() - origin.getZ());
13697    };
13698
13699    // namespaces
13700    var dwv = dwv || {};
13701    /** @namespace */
13702    dwv.image = dwv.image || {};
13703
13704    /**
13705     * Rescale Slope and Intercept
13706     * @constructor
13707     * @param slope
13708     * @param intercept
13709     */
13710    dwv.image.RescaleSlopeAndIntercept = function(slope, intercept) {
13711        /*// Check the rescale slope.
13712        if(typeof(slope) === 'undefined') {
13713            slope = 1;
13714        }
13715        // Check the rescale intercept.
13716        if(typeof(intercept) === 'undefined') {
13717            intercept = 0;
13718        }*/
13719
13720        /**
13721         * Get the slope of the RSI.
13722         * @return {Number} The slope of the RSI.
13723         */
13724        this.getSlope = function() {
13725            return slope;
13726        };
13727        /**
13728         * Get the intercept of the RSI.
13729         * @return {Number} The intercept of the RSI.
13730         */
13731        this.getIntercept = function() {
13732            return intercept;
13733        };
13734        /**
13735         * Apply the RSI on an input value.
13736         * @return {Number} The value to rescale.
13737         */
13738        this.apply = function(value) {
13739            return value * slope + intercept;
13740        };
13741    };
13742
13743    /**
13744     * Check for RSI equality.
13745     * @param {Object} rhs The other RSI to compare to.
13746     * @return {Boolean} True if both RSI are equal.
13747     */
13748    dwv.image.RescaleSlopeAndIntercept.prototype.equals = function(rhs) {
13749        return rhs !== null &&
13750            this.getSlope() === rhs.getSlope() &&
13751            this.getIntercept() === rhs.getIntercept();
13752    };
13753
13754    /**
13755     * Get a string representation of the RSI.
13756     * @return {String} The RSI as a string.
13757     */
13758    dwv.image.RescaleSlopeAndIntercept.prototype.toString = function() {
13759        return (this.getSlope() + ", " + this.getIntercept());
13760    };
13761
13762    /**
13763     * Is this RSI an ID RSI.
13764     * @return {Boolean} True if the RSI has a slope of 1 and no intercept.
13765     */
13766    dwv.image.RescaleSlopeAndIntercept.prototype.isID = function() {
13767        return (this.getSlope() === 1 && this.getIntercept() === 0);
13768    };
13769
13770    /**
13771     * Image class.
13772     * Usable once created, optional are:
13773     * - rescale slope and intercept (default 1:0),
13774     * - photometric interpretation (default MONOCHROME2),
13775     * - planar configuration (default RGBRGB...).
13776     * @constructor
13777     * @param {Object} geometry The geometry of the image.
13778     * @param {Array} buffer The image data as an array of frame buffers.
13779     * @param {Number} numberOfFrames The number of frames (optional, can be used
13780         to anticipate the final number after appends).
13781     */
13782    dwv.image.Image = function(geometry, buffer, numberOfFrames) {
13783        // use buffer length in not specified
13784        if (typeof numberOfFrames === "undefined") {
13785            numberOfFrames = buffer.length;
13786        }
13787
13788        /**
13789         * Get the number of frames.
13790         * @returns {Number} The number of frames.
13791         */
13792        this.getNumberOfFrames = function() {
13793            return numberOfFrames;
13794        };
13795
13796        /**
13797         * Rescale slope and intercept.
13798         * @private
13799         * @type Number
13800         */
13801        var rsis = [];
13802        // initialise RSIs
13803        for (var s = 0, nslices = geometry.getSize().getNumberOfSlices(); s < nslices; ++s) {
13804            rsis.push(new dwv.image.RescaleSlopeAndIntercept(1, 0));
13805        }
13806        /**
13807         * Flag to know if the RSIs are all identity (1,0).
13808         * @private
13809         * @type Boolean
13810         */
13811        var isIdentityRSI = true;
13812        /**
13813         * Flag to know if the RSIs are all equals.
13814         * @private
13815         * @type Boolean
13816         */
13817        var isConstantRSI = true;
13818        /**
13819         * Photometric interpretation (MONOCHROME, RGB...).
13820         * @private
13821         * @type String
13822         */
13823        var photometricInterpretation = "MONOCHROME2";
13824        /**
13825         * Planar configuration for RGB data (0:RGBRGBRGBRGB... or 1:RRR...GGG...BBB...).
13826         * @private
13827         * @type Number
13828         */
13829        var planarConfiguration = 0;
13830        /**
13831         * Number of components.
13832         * @private
13833         * @type Number
13834         */
13835        var numberOfComponents = buffer[0].length / (
13836            geometry.getSize().getTotalSize());
13837        /**
13838         * Meta information.
13839         * @private
13840         * @type Object
13841         */
13842        var meta = {};
13843
13844        /**
13845         * Data range.
13846         * @private
13847         * @type Object
13848         */
13849        var dataRange = null;
13850        /**
13851         * Rescaled data range.
13852         * @private
13853         * @type Object
13854         */
13855        var rescaledDataRange = null;
13856        /**
13857         * Histogram.
13858         * @private
13859         * @type Array
13860         */
13861        var histogram = null;
13862
13863        /**
13864         * Overlay.
13865         * @private
13866         * @type Array
13867         */
13868        var overlays = [];
13869
13870        /**
13871         * Set the first overlay.
13872         * @param {Array} over The first overlay.
13873         */
13874        this.setFirstOverlay = function(over) { overlays[0] = over; };
13875
13876        /**
13877         * Get the overlays.
13878         * @return {Array} The overlays array.
13879         */
13880        this.getOverlays = function() { return overlays; };
13881
13882        /**
13883         * Get the geometry of the image.
13884         * @return {Object} The size of the image.
13885         */
13886        this.getGeometry = function() { return geometry; };
13887
13888        /**
13889         * Get the data buffer of the image.
13890         * @todo dangerous...
13891         * @return {Array} The data buffer of the image.
13892         */
13893        this.getBuffer = function() { return buffer; };
13894        /**
13895         * Get the data buffer of the image.
13896         * @todo dangerous...
13897         * @return {Array} The data buffer of the frame.
13898         */
13899        this.getFrame = function(frame) { return buffer[frame]; };
13900
13901        /**
13902         * Get the rescale slope and intercept.
13903         * @param {Number} k The slice index.
13904         * @return {Object} The rescale slope and intercept.
13905         */
13906        this.getRescaleSlopeAndIntercept = function(k) { return rsis[k]; };
13907        /**
13908         * Set the rescale slope and intercept.
13909         * @param {Array} inRsi The input rescale slope and intercept.
13910         * @param {Number} k The slice index (optional).
13911         */
13912        this.setRescaleSlopeAndIntercept = function(inRsi, k) {
13913            if (typeof k === 'undefined') {
13914                k = 0;
13915            }
13916            rsis[k] = inRsi;
13917
13918            // update RSI flags
13919            isIdentityRSI = true;
13920            isConstantRSI = true;
13921            for (var s = 0, lens = rsis.length; s < lens; ++s) {
13922                if (!rsis[s].isID()) {
13923                    isIdentityRSI = false;
13924                }
13925                if (s > 0 && !rsis[s].equals(rsis[s - 1])) {
13926                    isConstantRSI = false;
13927                }
13928            }
13929        };
13930        /**
13931         * Are all the RSIs identity (1,0).
13932         * @return {Boolean} True if they are.
13933         */
13934        this.isIdentityRSI = function() { return isIdentityRSI; };
13935        /**
13936         * Are all the RSIs equal.
13937         * @return {Boolean} True if they are.
13938         */
13939        this.isConstantRSI = function() { return isConstantRSI; };
13940        /**
13941         * Get the photometricInterpretation of the image.
13942         * @return {String} The photometricInterpretation of the image.
13943         */
13944        this.getPhotometricInterpretation = function() { return photometricInterpretation; };
13945        /**
13946         * Set the photometricInterpretation of the image.
13947         * @pqrqm {String} interp The photometricInterpretation of the image.
13948         */
13949        this.setPhotometricInterpretation = function(interp) { photometricInterpretation = interp; };
13950        /**
13951         * Get the planarConfiguration of the image.
13952         * @return {Number} The planarConfiguration of the image.
13953         */
13954        this.getPlanarConfiguration = function() { return planarConfiguration; };
13955        /**
13956         * Set the planarConfiguration of the image.
13957         * @param {Number} config The planarConfiguration of the image.
13958         */
13959        this.setPlanarConfiguration = function(config) { planarConfiguration = config; };
13960        /**
13961         * Get the numberOfComponents of the image.
13962         * @return {Number} The numberOfComponents of the image.
13963         */
13964        this.getNumberOfComponents = function() { return numberOfComponents; };
13965
13966        /**
13967         * Get the meta information of the image.
13968         * @return {Object} The meta information of the image.
13969         */
13970        this.getMeta = function() { return meta; };
13971        /**
13972         * Set the meta information of the image.
13973         * @param {Object} rhs The meta information of the image.
13974         */
13975        this.setMeta = function(rhs) { meta = rhs; };
13976
13977        /**
13978         * Get value at offset. Warning: No size check...
13979         * @param {Number} offset The desired offset.
13980         * @param {Number} frame The desired frame.
13981         * @return {Number} The value at offset.
13982         */
13983        this.getValueAtOffset = function(offset, frame) {
13984            return buffer[frame][offset];
13985        };
13986
13987        /**
13988         * Clone the image.
13989         * @return {Image} A clone of this image.
13990         */
13991        this.clone = function() {
13992            // clone the image buffer
13993            var clonedBuffer = [];
13994            for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) {
13995                clonedBuffer[f] = buffer[f].slice(0);
13996            }
13997            // create the image copy
13998            var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer);
13999            // copy the RSIs
14000            var nslices = this.getGeometry().getSize().getNumberOfSlices();
14001            for (var k = 0; k < nslices; ++k) {
14002                copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept(k), k);
14003            }
14004            // copy extras
14005            copy.setPhotometricInterpretation(this.getPhotometricInterpretation());
14006            copy.setPlanarConfiguration(this.getPlanarConfiguration());
14007            copy.setMeta(this.getMeta());
14008            // return
14009            return copy;
14010        };
14011
14012        /**
14013         * Append a slice to the image.
14014         * @param {Image} The slice to append.
14015         * @return {Number} The number of the inserted slice.
14016         */
14017        this.appendSlice = function(rhs, frame) {
14018            // check input
14019            if (rhs === null) {
14020                throw new Error("Cannot append null slice");
14021            }
14022            var rhsSize = rhs.getGeometry().getSize();
14023            var size = geometry.getSize();
14024            if (rhsSize.getNumberOfSlices() !== 1) {
14025                throw new Error("Cannot append more than one slice");
14026            }
14027            if (size.getNumberOfColumns() !== rhsSize.getNumberOfColumns()) {
14028                throw new Error("Cannot append a slice with different number of columns");
14029            }
14030            if (size.getNumberOfRows() !== rhsSize.getNumberOfRows()) {
14031                throw new Error("Cannot append a slice with different number of rows");
14032            }
14033            if (photometricInterpretation !== rhs.getPhotometricInterpretation()) {
14034                throw new Error("Cannot append a slice with different photometric interpretation");
14035            }
14036            // all meta should be equal
14037            for (var key in meta) {
14038                if (meta[key] !== rhs.getMeta()[key]) {
14039                    throw new Error("Cannot append a slice with different " + key);
14040                }
14041            }
14042
14043            var f = (typeof frame === "undefined") ? 0 : frame;
14044
14045            // calculate slice size
14046            var mul = 1;
14047            if (photometricInterpretation === "RGB" || photometricInterpretation === "YBR_FULL_422") {
14048                mul = 3;
14049            }
14050            var sliceSize = mul * size.getSliceSize();
14051
14052            // create the new buffer
14053            var newBuffer = dwv.dicom.getTypedArray(
14054                buffer[f].BYTES_PER_ELEMENT * 8,
14055                meta.IsSigned ? 1 : 0,
14056                sliceSize * (size.getNumberOfSlices() + 1));
14057
14058            // append slice at new position
14059            var newSliceNb = geometry.getSliceIndex(rhs.getGeometry().getOrigin());
14060            if (newSliceNb === 0) {
14061                newBuffer.set(rhs.getFrame(f));
14062                newBuffer.set(buffer[f], sliceSize);
14063            } else if (newSliceNb === size.getNumberOfSlices()) {
14064                newBuffer.set(buffer[f]);
14065                newBuffer.set(rhs.getFrame(f), size.getNumberOfSlices() * sliceSize);
14066            } else {
14067                var offset = newSliceNb * sliceSize;
14068                newBuffer.set(buffer[f].subarray(0, offset - 1));
14069                newBuffer.set(rhs.getFrame(f), offset);
14070                newBuffer.set(buffer[f].subarray(offset), offset + sliceSize);
14071            }
14072
14073            // update geometry
14074            geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceNb);
14075            // update rsi
14076            rsis.splice(newSliceNb, 0, rhs.getRescaleSlopeAndIntercept(0));
14077
14078            // copy to class variables
14079            buffer[f] = newBuffer;
14080
14081            // insert overlay information of the slice to the image
14082            overlays.splice(newSliceNb, 0, rhs.getOverlays()[0]);
14083
14084            // return the appended slice number
14085            return newSliceNb;
14086        };
14087
14088        /**
14089         * Append a frame buffer to the image.
14090         * @param {Object} frameBuffer The frame buffer to append.
14091         */
14092        this.appendFrameBuffer = function(frameBuffer) {
14093            buffer.push(frameBuffer);
14094        };
14095
14096        /**
14097         * Get the data range.
14098         * @return {Object} The data range.
14099         */
14100        this.getDataRange = function() {
14101            if (!dataRange) {
14102                dataRange = this.calculateDataRange();
14103            }
14104            return dataRange;
14105        };
14106
14107        /**
14108         * Get the rescaled data range.
14109         * @return {Object} The rescaled data range.
14110         */
14111        this.getRescaledDataRange = function() {
14112            if (!rescaledDataRange) {
14113                rescaledDataRange = this.calculateRescaledDataRange();
14114            }
14115            return rescaledDataRange;
14116        };
14117
14118        /**
14119         * Get the histogram.
14120         * @return {Array} The histogram.
14121         */
14122        this.getHistogram = function() {
14123            if (!histogram) {
14124                var res = this.calculateHistogram();
14125                dataRange = res.dataRange;
14126                rescaledDataRange = res.rescaledDataRange;
14127                histogram = res.histogram;
14128            }
14129            return histogram;
14130        };
14131    };
14132
14133    /**
14134     * Get the value of the image at a specific coordinate.
14135     * @param {Number} i The X index.
14136     * @param {Number} j The Y index.
14137     * @param {Number} k The Z index.
14138     * @param {Number} f The frame number.
14139     * @return {Number} The value at the desired position.
14140     * Warning: No size check...
14141     */
14142    dwv.image.Image.prototype.getValue = function(i, j, k, f) {
14143        var frame = (f || 0);
14144        var index = new dwv.math.Index3D(i, j, k);
14145        return this.getValueAtOffset(this.getGeometry().indexToOffset(index), frame);
14146    };
14147
14148    /**
14149     * Get the rescaled value of the image at a specific coordinate.
14150     * @param {Number} i The X index.
14151     * @param {Number} j The Y index.
14152     * @param {Number} k The Z index.
14153     * @param {Number} f The frame number.
14154     * @return {Number} The rescaled value at the desired position.
14155     * Warning: No size check...
14156     */
14157    dwv.image.Image.prototype.getRescaledValue = function(i, j, k, f) {
14158        var frame = (f || 0);
14159        var val = this.getValue(i, j, k, frame);
14160        if (!this.isIdentityRSI()) {
14161            val = this.getRescaleSlopeAndIntercept(k).apply(val);
14162        }
14163        return val;
14164    };
14165
14166    /**
14167     * Calculate the data range of the image.
14168     * WARNING: for speed reasons, only calculated on the first frame...
14169     * @return {Object} The range {min, max}.
14170     */
14171    dwv.image.Image.prototype.calculateDataRange = function() {
14172        var size = this.getGeometry().getSize().getTotalSize();
14173        var nFrames = 1; //this.getNumberOfFrames();
14174        var min = this.getValueAtOffset(0, 0);
14175        var max = min;
14176        var value = 0;
14177        for (var f = 0; f < nFrames; ++f) {
14178            for (var i = 0; i < size; ++i) {
14179                value = this.getValueAtOffset(i, f);
14180                if (value > max) { max = value; }
14181                if (value < min) { min = value; }
14182            }
14183        }
14184        // return
14185        return { "min": min, "max": max };
14186    };
14187
14188    /**
14189     * Calculate the rescaled data range of the image.
14190     * WARNING: for speed reasons, only calculated on the first frame...
14191     * @return {Object} The range {min, max}.
14192     */
14193    dwv.image.Image.prototype.calculateRescaledDataRange = function() {
14194        if (this.isIdentityRSI()) {
14195            return this.getDataRange();
14196        } else if (this.isConstantRSI()) {
14197            var range = this.getDataRange();
14198            var resmin = this.getRescaleSlopeAndIntercept(0).apply(range.min);
14199            var resmax = this.getRescaleSlopeAndIntercept(0).apply(range.max);
14200            return {
14201                "min": ((resmin < resmax) ? resmin : resmax),
14202                "max": ((resmin > resmax) ? resmin : resmax)
14203            };
14204        } else {
14205            var size = this.getGeometry().getSize();
14206            var nFrames = 1; //this.getNumberOfFrames();
14207            var rmin = this.getRescaledValue(0, 0, 0);
14208            var rmax = rmin;
14209            var rvalue = 0;
14210            for (var f = 0, nframes = nFrames; f < nframes; ++f) {
14211                for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) {
14212                    for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) {
14213                        for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) {
14214                            rvalue = this.getRescaledValue(i, j, k, f);
14215                            if (rvalue > rmax) { rmax = rvalue; }
14216                            if (rvalue < rmin) { rmin = rvalue; }
14217                        }
14218                    }
14219                }
14220            }
14221            // return
14222            return { "min": rmin, "max": rmax };
14223        }
14224    };
14225
14226    /**
14227     * Calculate the histogram of the image.
14228     * @return {Object} The histogram, data range and rescaled data range.
14229     */
14230    dwv.image.Image.prototype.calculateHistogram = function() {
14231        var size = this.getGeometry().getSize();
14232        var histo = [];
14233        var min = this.getValue(0, 0, 0);
14234        var max = min;
14235        var value = 0;
14236        var rmin = this.getRescaledValue(0, 0, 0);
14237        var rmax = rmin;
14238        var rvalue = 0;
14239        for (var f = 0, nframes = this.getNumberOfFrames(); f < nframes; ++f) {
14240            for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) {
14241                for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) {
14242                    for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) {
14243                        value = this.getValue(i, j, k, f);
14244                        if (value > max) { max = value; }
14245                        if (value < min) { min = value; }
14246                        rvalue = this.getRescaleSlopeAndIntercept(k).apply(value);
14247                        if (rvalue > rmax) { rmax = rvalue; }
14248                        if (rvalue < rmin) { rmin = rvalue; }
14249                        histo[rvalue] = (histo[rvalue] || 0) + 1;
14250                    }
14251                }
14252            }
14253        }
14254        // set data range
14255        var dataRange = { "min": min, "max": max };
14256        var rescaledDataRange = { "min": rmin, "max": rmax };
14257        // generate data for plotting
14258        var histogram = [];
14259        for (var b = rmin; b <= rmax; ++b) {
14260            histogram.push([b, (histo[b] || 0)]);
14261        }
14262        // return
14263        return {
14264            'dataRange': dataRange,
14265            'rescaledDataRange': rescaledDataRange,
14266            'histogram': histogram
14267        };
14268    };
14269
14270    /**
14271     * Convolute the image with a given 2D kernel.
14272     * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix.
14273     * @return {Image} The convoluted image.
14274     * Note: Uses the raw buffer values.
14275     */
14276    dwv.image.Image.prototype.convolute2D = function(weights) {
14277        if (weights.length !== 9) {
14278            throw new Error("The convolution matrix does not have a length of 9; it has " + weights.length);
14279        }
14280
14281        var newImage = this.clone();
14282        var newBuffer = newImage.getBuffer();
14283
14284        var imgSize = this.getGeometry().getSize();
14285        var ncols = imgSize.getNumberOfColumns();
14286        var nrows = imgSize.getNumberOfRows();
14287        var nslices = imgSize.getNumberOfSlices();
14288        var nframes = this.getNumberOfFrames();
14289        var ncomp = this.getNumberOfComponents();
14290
14291        // adapt to number of component and planar configuration
14292        var factor = 1;
14293        var componentOffset = 1;
14294        var frameOffset = imgSize.getTotalSize();
14295        if (ncomp === 3) {
14296            frameOffset *= 3;
14297            if (this.getPlanarConfiguration() === 0) {
14298                factor = 3;
14299            } else {
14300                componentOffset = imgSize.getTotalSize();
14301            }
14302        }
14303
14304        // allow special indent for matrices
14305        /*jshint indent:false */
14306
14307        // default weight offset matrix
14308        var wOff = [];
14309        wOff[0] = (-ncols - 1) * factor;
14310        wOff[1] = (-ncols) * factor;
14311        wOff[2] = (-ncols + 1) * factor;
14312        wOff[3] = -factor;
14313        wOff[4] = 0;
14314        wOff[5] = 1 * factor;
14315        wOff[6] = (ncols - 1) * factor;
14316        wOff[7] = (ncols) * factor;
14317        wOff[8] = (ncols + 1) * factor;
14318
14319        // border weight offset matrices
14320        // borders are extended (see http://en.wikipedia.org/wiki/Kernel_%28image_processing%29)
14321
14322        // i=0, j=0
14323        var wOff00 = [];
14324        wOff00[0] = wOff[4];
14325        wOff00[1] = wOff[4];
14326        wOff00[2] = wOff[5];
14327        wOff00[3] = wOff[4];
14328        wOff00[4] = wOff[4];
14329        wOff00[5] = wOff[5];
14330        wOff00[6] = wOff[7];
14331        wOff00[7] = wOff[7];
14332        wOff00[8] = wOff[8];
14333        // i=0, j=*
14334        var wOff0x = [];
14335        wOff0x[0] = wOff[1];
14336        wOff0x[1] = wOff[1];
14337        wOff0x[2] = wOff[2];
14338        wOff0x[3] = wOff[4];
14339        wOff0x[4] = wOff[4];
14340        wOff0x[5] = wOff[5];
14341        wOff0x[6] = wOff[7];
14342        wOff0x[7] = wOff[7];
14343        wOff0x[8] = wOff[8];
14344        // i=0, j=nrows
14345        var wOff0n = [];
14346        wOff0n[0] = wOff[1];
14347        wOff0n[1] = wOff[1];
14348        wOff0n[2] = wOff[2];
14349        wOff0n[3] = wOff[4];
14350        wOff0n[4] = wOff[4];
14351        wOff0n[5] = wOff[5];
14352        wOff0n[6] = wOff[4];
14353        wOff0n[7] = wOff[4];
14354        wOff0n[8] = wOff[5];
14355
14356        // i=*, j=0
14357        var wOffx0 = [];
14358        wOffx0[0] = wOff[3];
14359        wOffx0[1] = wOff[4];
14360        wOffx0[2] = wOff[5];
14361        wOffx0[3] = wOff[3];
14362        wOffx0[4] = wOff[4];
14363        wOffx0[5] = wOff[5];
14364        wOffx0[6] = wOff[6];
14365        wOffx0[7] = wOff[7];
14366        wOffx0[8] = wOff[8];
14367        // i=*, j=* -> wOff
14368        // i=*, j=nrows
14369        var wOffxn = [];
14370        wOffxn[0] = wOff[0];
14371        wOffxn[1] = wOff[1];
14372        wOffxn[2] = wOff[2];
14373        wOffxn[3] = wOff[3];
14374        wOffxn[4] = wOff[4];
14375        wOffxn[5] = wOff[5];
14376        wOffxn[6] = wOff[3];
14377        wOffxn[7] = wOff[4];
14378        wOffxn[8] = wOff[5];
14379
14380        // i=ncols, j=0
14381        var wOffn0 = [];
14382        wOffn0[0] = wOff[3];
14383        wOffn0[1] = wOff[4];
14384        wOffn0[2] = wOff[4];
14385        wOffn0[3] = wOff[3];
14386        wOffn0[4] = wOff[4];
14387        wOffn0[5] = wOff[4];
14388        wOffn0[6] = wOff[6];
14389        wOffn0[7] = wOff[7];
14390        wOffn0[8] = wOff[7];
14391        // i=ncols, j=*
14392        var wOffnx = [];
14393        wOffnx[0] = wOff[0];
14394        wOffnx[1] = wOff[1];
14395        wOffnx[2] = wOff[1];
14396        wOffnx[3] = wOff[3];
14397        wOffnx[4] = wOff[4];
14398        wOffnx[5] = wOff[4];
14399        wOffnx[6] = wOff[6];
14400        wOffnx[7] = wOff[7];
14401        wOffnx[8] = wOff[7];
14402        // i=ncols, j=nrows
14403        var wOffnn = [];
14404        wOffnn[0] = wOff[0];
14405        wOffnn[1] = wOff[1];
14406        wOffnn[2] = wOff[1];
14407        wOffnn[3] = wOff[3];
14408        wOffnn[4] = wOff[4];
14409        wOffnn[5] = wOff[4];
14410        wOffnn[6] = wOff[3];
14411        wOffnn[7] = wOff[4];
14412        wOffnn[8] = wOff[4];
14413
14414        // restore indent for rest of method
14415        /*jshint indent:4 */
14416
14417        // loop vars
14418        var pixelOffset = 0;
14419        var newValue = 0;
14420        var wOffFinal = [];
14421        // go through the destination image pixels
14422        for (var f = 0; f < nframes; f++) {
14423            pixelOffset = f * frameOffset;
14424            for (var c = 0; c < ncomp; c++) {
14425                // special component offset
14426                pixelOffset += c * componentOffset;
14427                for (var k = 0; k < nslices; k++) {
14428                    for (var j = 0; j < nrows; j++) {
14429                        for (var i = 0; i < ncols; i++) {
14430                            wOffFinal = wOff;
14431                            // special border cases
14432                            if (i === 0 && j === 0) {
14433                                wOffFinal = wOff00;
14434                            } else if (i === 0 && j === (nrows - 1)) {
14435                                wOffFinal = wOff0n;
14436                            } else if (i === (ncols - 1) && j === 0) {
14437                                wOffFinal = wOffn0;
14438                            } else if (i === (ncols - 1) && j === (nrows - 1)) {
14439                                wOffFinal = wOffnn;
14440                            } else if (i === 0 && j !== (nrows - 1) && j !== 0) {
14441                                wOffFinal = wOff0x;
14442                            } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) {
14443                                wOffFinal = wOffnx;
14444                            } else if (i !== 0 && i !== (ncols - 1) && j === 0) {
14445                                wOffFinal = wOffx0;
14446                            } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) {
14447                                wOffFinal = wOffxn;
14448                            }
14449
14450                            // calculate the weighed sum of the source image pixels that
14451                            // fall under the convolution matrix
14452                            newValue = 0;
14453                            for (var wi = 0; wi < 9; ++wi) {
14454                                newValue += this.getValueAtOffset(pixelOffset + wOffFinal[wi], f) * weights[wi];
14455                            }
14456                            newBuffer[f][pixelOffset] = newValue;
14457                            // increment pixel offset
14458                            pixelOffset += factor;
14459                        }
14460                    }
14461                }
14462            }
14463        }
14464        return newImage;
14465    };
14466
14467    /**
14468     * Transform an image using a specific operator.
14469     * WARNING: no size check!
14470     * @param {Function} operator The operator to use when transforming.
14471     * @return {Image} The transformed image.
14472     * Note: Uses the raw buffer values.
14473     */
14474    dwv.image.Image.prototype.transform = function(operator) {
14475        var newImage = this.clone();
14476        var newBuffer = newImage.getBuffer();
14477        for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) {
14478            for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) {
14479                newBuffer[f][i] = operator(newImage.getValueAtOffset(i, f));
14480            }
14481        }
14482        return newImage;
14483    };
14484
14485    /**
14486     * Compose this image with another one and using a specific operator.
14487     * WARNING: no size check!
14488     * @param {Image} rhs The image to compose with.
14489     * @param {Function} operator The operator to use when composing.
14490     * @return {Image} The composed image.
14491     * Note: Uses the raw buffer values.
14492     */
14493    dwv.image.Image.prototype.compose = function(rhs, operator) {
14494        var newImage = this.clone();
14495        var newBuffer = newImage.getBuffer();
14496        for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) {
14497            for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) {
14498                // using the operator on the local buffer, i.e. the latest (not original) data
14499                newBuffer[f][i] = Math.floor(operator(this.getValueAtOffset(i, f), rhs.getValueAtOffset(i, f)));
14500            }
14501        }
14502        return newImage;
14503    };
14504
14505    /**
14506     * Quantify a line according to image information.
14507     * @param {Object} line The line to quantify.
14508     * @return {Object} A quantification object.
14509     */
14510    dwv.image.Image.prototype.quantifyLine = function(line) {
14511        var quant = {};
14512        // length
14513        var spacing = this.getGeometry().getSpacing();
14514        var length = line.getWorldLength(spacing.getColumnSpacing(),
14515            spacing.getRowSpacing());
14516        if (length !== null) {
14517            quant.length = { "value": length, "unit": dwv.i18n("unit.mm") };
14518        }
14519        // return
14520        return quant;
14521    };
14522
14523    /**
14524     * Quantify a rectangle according to image information.
14525     * @param {Object} rect The rectangle to quantify.
14526     * @return {Object} A quantification object.
14527     */
14528    dwv.image.Image.prototype.quantifyRect = function(rect) {
14529        var quant = {};
14530        // surface
14531        var spacing = this.getGeometry().getSpacing();
14532        var surface = rect.getWorldSurface(spacing.getColumnSpacing(),
14533            spacing.getRowSpacing());
14534        if (surface !== null) {
14535            quant.surface = { "value": surface / 100, "unit": dwv.i18n("unit.cm2") };
14536        }
14537        // stats
14538        var subBuffer = [];
14539        var minJ = parseInt(rect.getBegin().getY(), 10);
14540        var maxJ = parseInt(rect.getEnd().getY(), 10);
14541        var minI = parseInt(rect.getBegin().getX(), 10);
14542        var maxI = parseInt(rect.getEnd().getX(), 10);
14543        for (var j = minJ; j < maxJ; ++j) {
14544            for (var i = minI; i < maxI; ++i) {
14545                subBuffer.push(this.getValue(i, j, 0));
14546            }
14547        }
14548        var quantif = dwv.math.getStats(subBuffer);
14549        quant.min = { "value": quantif.min, "unit": "" };
14550        quant.max = { "value": quantif.max, "unit": "" };
14551        quant.mean = { "value": quantif.mean, "unit": "" };
14552        quant.stdDev = { "value": quantif.stdDev, "unit": "" };
14553        // return
14554        return quant;
14555    };
14556
14557    /**
14558     * Quantify an ellipse according to image information.
14559     * @param {Object} ellipse The ellipse to quantify.
14560     * @return {Object} A quantification object.
14561     */
14562    dwv.image.Image.prototype.quantifyEllipse = function(ellipse) {
14563        var quant = {};
14564        // surface
14565        var spacing = this.getGeometry().getSpacing();
14566        var surface = ellipse.getWorldSurface(spacing.getColumnSpacing(),
14567            spacing.getRowSpacing());
14568        if (surface !== null) {
14569            quant.surface = { "value": surface / 100, "unit": dwv.i18n("unit.cm2") };
14570        }
14571        // return
14572        return quant;
14573    };
14574
14575    /**
14576     * {@link dwv.image.Image} factory.
14577     * @constructor
14578     */
14579    dwv.image.ImageFactory = function() {};
14580
14581    /**
14582     * Get an {@link dwv.image.Image} object from the read DICOM file.
14583     * @param {Object} dicomElements The DICOM tags.
14584     * @param {Array} pixelBuffer The pixel buffer.
14585     * @return {View} A new Image.
14586     */
14587    dwv.image.ImageFactory.prototype.create = function(dicomElements, pixelBuffer) {
14588        // columns
14589        var columns = dicomElements.getFromKey("x00280011");
14590        if (!columns) {
14591            throw new Error("Missing or empty DICOM image number of columns");
14592        }
14593        // rows
14594        var rows = dicomElements.getFromKey("x00280010");
14595        if (!rows) {
14596            throw new Error("Missing or empty DICOM image number of rows");
14597        }
14598        // image size
14599        var size = new dwv.image.Size(columns, rows);
14600
14601        // spacing
14602        var rowSpacing = null;
14603        var columnSpacing = null;
14604        // PixelSpacing
14605        var pixelSpacing = dicomElements.getFromKey("x00280030");
14606        // ImagerPixelSpacing
14607        var imagerPixelSpacing = dicomElements.getFromKey("x00181164");
14608        if (pixelSpacing && pixelSpacing[0] && pixelSpacing[1]) {
14609            rowSpacing = parseFloat(pixelSpacing[0]);
14610            columnSpacing = parseFloat(pixelSpacing[1]);
14611        } else if (imagerPixelSpacing && imagerPixelSpacing[0] && imagerPixelSpacing[1]) {
14612            rowSpacing = parseFloat(imagerPixelSpacing[0]);
14613            columnSpacing = parseFloat(imagerPixelSpacing[1]);
14614        }
14615        // image spacing
14616        var spacing = new dwv.image.Spacing(columnSpacing, rowSpacing);
14617
14618        // TransferSyntaxUID
14619        var transferSyntaxUID = dicomElements.getFromKey("x00020010");
14620        var syntax = dwv.dicom.cleanString(transferSyntaxUID);
14621        var jpeg2000 = dwv.dicom.isJpeg2000TransferSyntax(syntax);
14622        var jpegBase = dwv.dicom.isJpegBaselineTransferSyntax(syntax);
14623        var jpegLoss = dwv.dicom.isJpegLosslessTransferSyntax(syntax);
14624
14625        // slice position
14626        var slicePosition = new Array(0, 0, 0);
14627        // ImagePositionPatient
14628        var imagePositionPatient = dicomElements.getFromKey("x00200032");
14629        if (imagePositionPatient) {
14630            slicePosition = [parseFloat(imagePositionPatient[0]),
14631                parseFloat(imagePositionPatient[1]),
14632                parseFloat(imagePositionPatient[2])
14633            ];
14634        }
14635
14636        // slice orientation
14637        var imageOrientationPatient = dicomElements.getFromKey("x00200037");
14638        var orientationMatrix;
14639        if (imageOrientationPatient) {
14640            var rowCosines = new dwv.math.Vector3D(parseFloat(imageOrientationPatient[0]),
14641                parseFloat(imageOrientationPatient[1]),
14642                parseFloat(imageOrientationPatient[2]));
14643            var colCosines = new dwv.math.Vector3D(parseFloat(imageOrientationPatient[3]),
14644                parseFloat(imageOrientationPatient[4]),
14645                parseFloat(imageOrientationPatient[5]));
14646            var normal = rowCosines.crossProduct(colCosines);
14647            orientationMatrix = new dwv.math.Matrix33(
14648                rowCosines.getX(), rowCosines.getY(), rowCosines.getZ(),
14649                colCosines.getX(), colCosines.getY(), colCosines.getZ(),
14650                normal.getX(), normal.getY(), normal.getZ());
14651        }
14652
14653        // geometry
14654        var origin = new dwv.math.Point3D(slicePosition[0], slicePosition[1], slicePosition[2]);
14655        var geometry = new dwv.image.Geometry(origin, size, spacing, orientationMatrix);
14656
14657        // image
14658        var image = new dwv.image.Image(geometry, pixelBuffer);
14659        // PhotometricInterpretation
14660        var photometricInterpretation = dicomElements.getFromKey("x00280004");
14661        if (photometricInterpretation) {
14662            var photo = dwv.dicom.cleanString(photometricInterpretation).toUpperCase();
14663            // jpeg decoders output RGB data
14664            if ((jpeg2000 || jpegBase || jpegLoss) &&
14665                (photo !== "MONOCHROME1" && photo !== "MONOCHROME2")) {
14666                photo = "RGB";
14667            }
14668            image.setPhotometricInterpretation(photo);
14669        }
14670        // PlanarConfiguration
14671        var planarConfiguration = dicomElements.getFromKey("x00280006");
14672        if (planarConfiguration) {
14673            image.setPlanarConfiguration(planarConfiguration);
14674        }
14675
14676        // rescale slope and intercept
14677        var slope = 1;
14678        // RescaleSlope
14679        var rescaleSlope = dicomElements.getFromKey("x00281053");
14680        if (rescaleSlope) {
14681            slope = parseFloat(rescaleSlope);
14682        }
14683        var intercept = 0;
14684        // RescaleIntercept
14685        var rescaleIntercept = dicomElements.getFromKey("x00281052");
14686        if (rescaleIntercept) {
14687            intercept = parseFloat(rescaleIntercept);
14688        }
14689        var rsi = new dwv.image.RescaleSlopeAndIntercept(slope, intercept);
14690        image.setRescaleSlopeAndIntercept(rsi);
14691
14692        // meta information
14693        var meta = {};
14694        // Modality
14695        var modality = dicomElements.getFromKey("x00080060");
14696        if (modality) {
14697            meta.Modality = modality;
14698        }
14699        // StudyInstanceUID
14700        var studyInstanceUID = dicomElements.getFromKey("x0020000D");
14701        if (studyInstanceUID) {
14702            meta.StudyInstanceUID = studyInstanceUID;
14703        }
14704        // SeriesInstanceUID
14705        var seriesInstanceUID = dicomElements.getFromKey("x0020000E");
14706        if (seriesInstanceUID) {
14707            meta.SeriesInstanceUID = seriesInstanceUID;
14708        }
14709        // BitsStored
14710        var bitsStored = dicomElements.getFromKey("x00280101");
14711        if (bitsStored) {
14712            meta.BitsStored = parseInt(bitsStored, 10);
14713        }
14714        // PixelRepresentation -> is signed
14715        var pixelRepresentation = dicomElements.getFromKey("x00280103");
14716        meta
14716.IsSigned = false;
14717        if (pixelRepresentation) {
14718            meta.IsSigned = (pixelRepresentation === 1);
14719        }
14720        image.setMeta(meta);
14721
14722        // overlay
14723        image.setFirstOverlay(dwv.gui.info.createOverlays(dicomElements));
14724
14725        return image;
14726    };
14727
14728    // namespaces
14729    var dwv = dwv || {};
14730    dwv.image = dwv.image || {};
14731    /** @namespace */
14732    dwv.image.lut = dwv.image.lut || {};
14733
14734    /**
14735     * Rescale LUT class.
14736     * @constructor
14737     * @param {Object} rsi The rescale slope and intercept.
14738     * @param {Number} bitsStored The number of bits used to store the data.
14739     */
14740    dwv.image.lut.Rescale = function(rsi, bitsStored) {
14741        /**
14742         * The internal array.
14743         * @private
14744         * @type Array
14745         */
14746        var lut = null;
14747
14748        /**
14749         * Flag to know if the lut is ready or not.
14750         * @private
14751         * @type Boolean
14752         */
14753        var isReady = false;
14754
14755        /**
14756         * The size of the LUT array.
14757         * @private
14758         * @type Number
14759         */
14760        var length = Math.pow(2, bitsStored);
14761
14762        /**
14763         * Get the Rescale Slope and Intercept (RSI).
14764         * @return {Object} The rescale slope and intercept.
14765         */
14766        this.getRSI = function() { return rsi; };
14767
14768        /**
14769         * Is the lut ready to use or not? If not, the user must
14770         * call 'initialise'.
14771         * @return {Boolean} True if the lut is ready to use.
14772         */
14773        this.isReady = function() { return isReady; };
14774
14775        /**
14776         * Initialise the LUT.
14777         */
14778        this.initialise = function() {
14779            // check if already initialised
14780            if (isReady) {
14781                return;
14782            }
14783            // create lut and fill it
14784            lut = new Float32Array(length);
14785            for (var i = 0; i < length; ++i) {
14786                lut[i] = rsi.apply(i);
14787            }
14788            // update ready flag
14789            isReady = true;
14790        };
14791
14792        /**
14793         * Get the length of the LUT array.
14794         * @return {Number} The length of the LUT array.
14795         */
14796        this.getLength = function() { return length; };
14797
14798        /**
14799         * Get the value of the LUT at the given offset.
14800         * @return {Number} The value of the LUT at the given offset.
14801         */
14802        this.getValue = function(offset) {
14803            return lut[offset];
14804        };
14805    };
14806
14807    /**
14808     * Window LUT class.
14809     * @constructor
14810     * @param {Number} rescaleLut The associated rescale LUT.
14811     * @param {Boolean} isSigned Flag to know if the data is signed or not.
14812     */
14813    dwv.image.lut.Window = function(rescaleLut, isSigned) {
14814        /**
14815         * The internal array: Uint8ClampedArray clamps between 0 and 255.
14816         * @private
14817         * @type Array
14818         */
14819        var lut = null;
14820
14821        /**
14822         * The window level.
14823         * @private
14824         * @type {Object}
14825         */
14826        var windowLevel = null;
14827
14828        /**
14829         * Flag to know if the lut is ready or not.
14830         * @private
14831         * @type Boolean
14832         */
14833        var isReady = false;
14834
14835        /**
14836         * Shift for signed data.
14837         * @private
14838         * @type Number
14839         */
14840        var signedShift = 0;
14841
14842        /**
14843         * Get the window / level.
14844         * @return {Object} The window / level.
14845         */
14846        this.getWindowLevel = function() { return windowLevel; };
14847        /**
14848         * Get the signed flag.
14849         * @return {Boolean} The signed flag.
14850         */
14851        this.isSigned = function() { return isSigned; };
14852        /**
14853         * Get the rescale lut.
14854         * @return {Object} The rescale lut.
14855         */
14856        this.getRescaleLut = function() { return rescaleLut; };
14857
14858        /**
14859         * Is the lut ready to use or not? If not, the user must
14860         * call 'update'.
14861         * @return {Boolean} True if the lut is ready to use.
14862         */
14863        this.isReady = function() { return isReady; };
14864
14865        /**
14866         * Set the window center and width.
14867         * @param {Object} wl The window level.
14868         */
14869        this.setWindowLevel = function(wl) {
14870            // store the window values
14871            windowLevel = wl;
14872            // possible signed shift
14873            signedShift = 0;
14874            windowLevel.setSignedOffset(0);
14875            if (isSigned) {
14876                var size = rescaleLut.getLength();
14877                signedShift = size / 2;
14878                windowLevel.setSignedOffset(rescaleLut.getRSI().getSlope() * signedShift);
14879            }
14880            // update ready flag
14881            isReady = false;
14882        };
14883
14884        /**
14885         * Update the lut if needed..
14886         */
14887        this.update = function() {
14888            // check if we need to update
14889            if (isReady) {
14890                return;
14891            }
14892
14893            // check rescale lut
14894            if (!rescaleLut.isReady()) {
14895                rescaleLut.initialise();
14896            }
14897            // create window lut
14898            var size = rescaleLut.getLength();
14899            if (!lut) {
14900                // use clamped array (polyfilled in browser.js)
14901                lut = new Uint8ClampedArray(size);
14902            }
14903            // by default WindowLevel returns a value in the [0,255] range
14904            // this is ok with regular Arrays and ClampedArray.
14905            for (var i = 0; i < size; ++i) {
14906                lut[i] = windowLevel.apply(rescaleLut.getValue(i));
14907            }
14908
14909            // update ready flag
14910            isReady = true;
14911        };
14912
14913        /**
14914         * Get the length of the LUT array.
14915         * @return {Number} The length of the LUT array.
14916         */
14917        this.getLength = function() { return lut.length; };
14918
14919        /**
14920         * Get the value of the LUT at the given offset.
14921         * @return {Number} The value of the LUT at the given offset.
14922         */
14923        this.getValue = function(offset) {
14924            return lut[offset + signedShift];
14925        };
14926    };
14927
14928    /**
14929     * Lookup tables for image colour display.
14930     */
14931
14932    dwv.image.lut.range_max = 256;
14933
14934    dwv.image.lut.buildLut = function(func) {
14935        var lut = [];
14936        for (var i = 0; i < dwv.image.lut.range_max; ++i) {
14937            lut.push(func(i));
14938        }
14939        return lut;
14940    };
14941
14942    dwv.image.lut.max = function( /*i*/ ) {
14943        return dwv.image.lut.range_max - 1;
14944    };
14945
14946    dwv.image.lut.maxFirstThird = function(i) {
14947        if (i < dwv.image.lut.range_max / 3) {
14948            return dwv.image.lut.range_max - 1;
14949        }
14950        return 0;
14951    };
14952
14953    dwv.image.lut.maxSecondThird = function(i) {
14954        var third = dwv.image.lut.range_max / 3;
14955        if (i >= third && i < 2 * third) {
14956            return dwv.image.lut.range_max - 1;
14957        }
14958        return 0;
14959    };
14960
14961    dwv.image.lut.maxThirdThird = function(i) {
14962        if (i >= 2 * dwv.image.lut.range_max / 3) {
14963            return dwv.image.lut.range_max - 1;
14964        }
14965        return 0;
14966    };
14967
14968    dwv.image.lut.toMaxFirstThird = function(i) {
14969        var val = i * 3;
14970        if (val > dwv.image.lut.range_max - 1) {
14971            return dwv.image.lut.range_max - 1;
14972        }
14973        return val;
14974    };
14975
14976    dwv.image.lut.toMaxSecondThird = function(i) {
14977        var third = dwv.image.lut.range_max / 3;
14978        var val = 0;
14979        if (i >= third) {
14980            val = (i - third) * 3;
14981            if (val > dwv.image.lut.range_max - 1) {
14982                return dwv.image.lut.range_max - 1;
14983            }
14984        }
14985        return val;
14986    };
14987
14988    dwv.image.lut.toMaxThirdThird = function(i) {
14989        var third = dwv.image.lut.range_max / 3;
14990        var val = 0;
14991        if (i >= 2 * third) {
14992            val = (i - 2 * third) * 3;
14993            if (val > dwv.image.lut.range_max - 1) {
14994                return dwv.image.lut.range_max - 1;
14995            }
14996        }
14997        return val;
14998    };
14999
15000    dwv.image.lut.zero = function( /*i*/ ) {
15001        return 0;
15002    };
15003
15004    dwv.image.lut.id = function(i) {
15005        return i;
15006    };
15007
15008    dwv.image.lut.invId = function(i) {
15009        return (dwv.image.lut.range_max - 1) - i;
15010    };
15011
15012    // plain
15013    dwv.image.lut.plain = {
15014        "red": dwv.image.lut.buildLut(dwv.image.lut.id),
15015        "green": dwv.image.lut.buildLut(dwv.image.lut.id),
15016        "blue": dwv.image.lut.buildLut(dwv.image.lut.id)
15017    };
15018
15019    // inverse plain
15020    dwv.image.lut.invPlain = {
15021        "red": dwv.image.lut.buildLut(dwv.image.lut.invId),
15022        "green": dwv.image.lut.buildLut(dwv.image.lut.invId),
15023        "blue": dwv.image.lut.buildLut(dwv.image.lut.invId)
15024    };
15025
15026    //rainbow
15027    dwv.image.lut.rainbow = {
15028        "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, 
15028128, 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],
15029        "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],
15030        "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]
15031    };
15032
15033    // hot
15034    dwv.image.lut.hot = {
15035        "red": dwv.image.lut.buildLut(dwv.image.lut.toMaxFirstThird),
15036        "green": dwv.image.lut.buildLut(dwv.image.lut.toMaxSecondThird),
15037        "blue": dwv.image.lut.buildLut(dwv.image.lut.toMaxThirdThird)
15038    };
15039
15040    // hot iron
15041    dwv.image.lut.hot_iron = {
15042        "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],
15043        "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, 
15043134, 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],
15044        "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]
15045    };
15046
15047    // pet
15048    dwv.image.lut.pet = {
15049        "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],
15050        "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],
15051        "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, 
15051128, 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]
15052    };
15053
15054    // hot metal blue
15055    dwv.image.lut.hot_metal_blue = {
15056        "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],
15057        "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],
15058        "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]
15059    };
15060
15061    // pet 20 step
15062    dwv.image.lut.pet_20step = {
15063        "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],
15064        "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],
15065        "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]
15066    };
15067
15068    // test
15069    dwv.image.lut.test = {
15070        "red": dwv.image.lut.buildLut(dwv.image.lut.id),
15071        "green": dwv.image.lut.buildLut(dwv.image.lut.zero),
15072        "blue": dwv.image.lut.buildLut(dwv.image.lut.zero)
15073    };
15074
15075    //red
15076    /*dwv.image.lut.red = {
15077       "red":   dwv.image.lut.buildLut(dwv.image.lut.max),
15078       "green": dwv.image.lut.buildLut(dwv.image.lut.id),
15079       "blue":  dwv.image.lut.buildLut(dwv.image.lut.id)
15080    };*/
15081
15082    // namespaces
15083    var dwv = dwv || {};
15084    dwv.image = dwv.image || {};
15085
15086    /**
15087     * WindowLevel class.
15088     * References:
15089     * - 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}
15090     * Pseudo-code:
15091     *  if (x <= c - 0.5 - (w-1)/2), then y = ymin
15092     *  else if (x > c - 0.5 + (w-1)/2), then y = ymax,
15093     *  else y = ((x - (c - 0.5)) / (w-1) + 0.5) * (ymax - ymin) + ymin
15094     */
15095    dwv.image.WindowLevel = function(center, width) {
15096        // avoid zero width
15097        if (width === 0) {
15098            throw new Error("A window level with a width of zero is not possible.");
15099        }
15100
15101        /**
15102         * Signed data offset.
15103         * @private
15104         * @type Number
15105         */
15106        var signedOffset = 0;
15107        /**
15108         * Output value minimum.
15109         * @private
15110         * @type Number
15111         */
15112        var ymin = 0;
15113        /**
15114         * Output value maximum.
15115         * @private
15116         * @type Number
15117         */
15118        var ymax = 255;
15119
15120        /**
15121         * Input value minimum (calculated).
15122         * @private
15123         * @type Number
15124         */
15125        var xmin = null;
15126        /**
15127         * Input value maximum (calculated).
15128         * @private
15129         * @type Number
15130         */
15131        var xmax = null;
15132        /**
15133         * Window level equation slope (calculated).
15134         * @private
15135         * @type Number
15136         */
15137        var slope = null;
15138        /**
15139         * Window level equation intercept (calculated).
15140         * @private
15141         * @type Number
15142         */
15143        var inter = null;
15144
15145        /**
15146         * Initialise members.
15147         */
15148        function init() {
15149            var c = center + signedOffset;
15150            // from the standard
15151            xmin = c - 0.5 - ((width - 1) / 2);
15152            xmax = c - 0.5 + ((width - 1) / 2);
15153            // develop the equation:
15154            // y = ( ( x - (c - 0.5) ) / (w-1) + 0.5 ) * (ymax - ymin) + ymin
15155            // y = ( x / (w-1) ) * (ymax - ymin) + ( -(c - 0.5) / (w-1) + 0.5 ) * (ymax - ymin) + ymin
15156            slope = (ymax - ymin) / (width - 1);
15157            inter = (-(c - 0.5) / (width - 1) + 0.5) * (ymax - ymin) + ymin;
15158        }
15159
15160        // call init
15161        init();
15162
15163        /**
15164         * Get the window center.
15165         * @return {Number} The window center.
15166         */
15167        this.getCenter = function() { return center; };
15168        /**
15169         * Get the window width.
15170         * @return {Number} The window width.
15171         */
15172        this.getWidth = function() { return width; };
15173
15174        /**
15175         * Set the output value range.
15176         * @param {Number} min The output value minimum.
15177         * @param {Number} max The output value maximum.
15178         */
15179        this.setRange = function(min, max) {
15180            ymin = parseInt(min, 10);
15181            ymax = parseInt(max, 10);
15182            // re-initialise
15183            init();
15184        };
15185        /**
15186         * Set the signed offset.
15187         * @param {Number} The signed data offset, typically: slope * ( size / 2).
15188         */
15189        this.setSignedOffset = function(offset) {
15190            signedOffset = offset;
15191            // re-initialise
15192            init();
15193        };
15194
15195        /**
15196         * Apply the window level on an input value.
15197         * @param {Number} The value to rescale as an integer.
15198         * @return {Number} The leveled value, in the
15199         *  [ymin, ymax] range (default [0,255]).
15200         */
15201        this.apply = function(value) {
15202            if (value <= xmin) {
15203                return ymin;
15204            } else if (value > xmax) {
15205                return ymax;
15206            } else {
15207                return parseInt(((value * slope) + inter), 10);
15208            }
15209        };
15210
15211    };
15212
15213    /**
15214     * Check for window level equality.
15215     * @param {Object} rhs The other window level to compare to.
15216     * @return {Boolean} True if both window level are equal.
15217     */
15218    dwv.image.WindowLevel.prototype.equals = function(rhs) {
15219        return rhs !== null &&
15220            this.getCenter() === rhs.getCenter() &&
15221            this.getWidth() === rhs.getWidth();
15222    };
15223
15224    /**
15225     * Get a string representation of the window level.
15226     * @return {String} The window level as a string.
15227     */
15228    dwv.image.WindowLevel.prototype.toString = function() {
15229        return (this.getCenter() + ", " + this.getWidth());
15230    };
15231
15232    /**
15233     * View class.
15234     * @constructor
15235     * @param {Image} image The associated image.
15236     * Need to set the window lookup table once created
15237     * (either directly or with helper methods).
15238     */
15239    dwv.image.View = function(image) {
15240        /**
15241         * Window lookup tables, indexed per Rescale Slope and Intercept (RSI).
15242         * @private
15243         * @type Window
15244         */
15245        var windowLuts = {};
15246
15247        /**
15248         * Window presets.
15249         * Minmax will be filled at first use (see view.setWindowLevelPreset).
15250         * @private
15251         * @type Object
15252         */
15253        var windowPresets = { "minmax": { "name": "minmax" } };
15254
15255        /**
15256         * Current window preset name.
15257         * @private
15258         * @type String
15259         */
15260        var currentPresetName = null;
15261
15262        /**
15263         * colour map.
15264         * @private
15265         * @type Object
15266         */
15267        var colourMap = dwv.image.lut.plain;
15268        /**
15269         * Current position.
15270         * @private
15271         * @type Object
15272         */
15273        var currentPosition = { "i": 0, "j": 0, "k": 0 };
15274        /**
15275         * Current frame. Zero based.
15276         * @private
15277         * @type Number
15278         */
15279        var currentFrame = null;
15280
15281        /**
15282         * Get the associated image.
15283         * @return {Image} The associated image.
15284         */
15285        this.getImage = function() { return image; };
15286        /**
15287         * Set the associated image.
15288         * @param {Image} inImage The associated image.
15289         */
15290        this.setImage = function(inImage) { image = inImage; };
15291
15292        /**
15293         * Get the window LUT of the image.
15294         * Warning: can be undefined in no window/level was set.
15295         * @param {Object} rsi Optional image rsi, will take the one of the current slice otherwise.
15296         * @return {Window} The window LUT of the image.
15297         */
15298        this.getCurrentWindowLut = function(rsi) {
15299            var sliceNumber = this.getCurrentPosition().k;
15300            // use current rsi if not provided
15301            if (typeof rsi === "undefined") {
15302                rsi = image.getRescaleSlopeAndIntercept(sliceNumber);
15303            }
15304            // get the lut
15305            var wlut = windowLuts[rsi.toString()];
15306
15307            // special case for 'perslice' presets
15308            if (currentPresetName &&
15309                typeof windowPresets[currentPresetName] !== "undefined" &&
15310                typeof windowPresets[currentPresetName].perslice !== "undefined" &&
15311                windowPresets[currentPresetName].perslice === true) {
15312                // get the preset for this slice
15313                var wl = windowPresets[currentPresetName].wl[sliceNumber];
15314                // apply it if different from previous
15315                if (!wlut.getWindowLevel().equals(wl)) {
15316                    // previous values
15317                    var previousWidth = wlut.getWindowLevel().getWidth();
15318                    var previousCenter = wlut.getWindowLevel().getCenter();
15319                    // set slice window level
15320                    wlut.setWindowLevel(wl);
15321                    // fire event
15322                    if (previousWidth !== wl.getWidth()) {
15323                        this.fireEvent({
15324                            "type": "wl-width-change",
15325                            "wc": wl.getCenter(),
15326                            "ww": wl.getWidth(),
15327                            "skipGenerate": true
15328                        });
15329                    }
15330                    if (previousCenter !== wl.getCenter()) {
15331                        this.fireEvent({
15332                            "type": "wl-center-change",
15333                            "wc": wl.getCenter(),
15334                            "ww": wl.getWidth(),
15335                            "skipGenerate": true
15336                        });
15337                    }
15338                }
15339            }
15340
15341            // update in case of wl change
15342            // TODO: should not be run in a getter...
15343            wlut.update();
15344
15345            // return
15346            return wlut;
15347        };
15348        /**
15349         * Add the window LUT to the list.
15350         * @param {Window} wlut The window LUT of the image.
15351         */
15352        this.addWindowLut = function(wlut) {
15353            var rsi = wlut.getRescaleLut().getRSI();
15354            windowLuts[rsi.toString()] = wlut;
15355        };
15356
15357        /**
15358         * Get the window presets.
15359         * @return {Object} The window presets.
15360         */
15361        this.getWindowPresets = function() {
15362            return windowPresets;
15363        };
15364
15365        /**
15366         * Get the window presets names.
15367         * @return {Object} The list of window presets names.
15368         */
15369        this.getWindowPresetsNames = function() {
15370            return Object.keys(windowPresets);
15371        };
15372
15373        /**
15374         * Set the window presets.
15375         * @param {Object} presets The window presets.
15376         */
15377        this.setWindowPresets = function(presets) {
15378            windowPresets = presets;
15379        };
15380
15381        /**
15382         * Set the default colour map.
15383         * @param {Object} map The colour map.
15384         */
15385        this.setDefaultColourMap = function(map) {
15386            colourMap = map;
15387        };
15388
15389        /**
15390         * Add window presets to the existing ones.
15391         * @param {Object} presets The window presets.
15392         * @param {Number} k The slice the preset belong to.
15393         */
15394        this.addWindowPresets = function(presets, k) {
15395            var keys = Object.keys(presets);
15396            var key = null;
15397            for (var i = 0; i < keys.length; ++i) {
15398                key = keys[i];
15399                if (typeof windowPresets[key] !== "undefined") {
15400                    if (typeof windowPresets[key].perslice !== "undefined" &&
15401                        windowPresets[key].perslice === true) {
15402                        // use first new preset wl...
15403                        windowPresets[key].wl.splice(k, 0, presets[key].wl[0]);
15404                    } else {
15405                        windowPresets[key] = presets[key];
15406                    }
15407                } else {
15408                    // add new
15409                    windowPresets[key] = presets[key];
15410                }
15411            }
15412        };
15413
15414        /**
15415         * Get the colour map of the image.
15416         * @return {Object} The colour map of the image.
15417         */
15418        this.getColourMap = function() { return colourMap; };
15419        /**
15420         * Set the colour map of the image.
15421         * @param {Object} map The colour map of the image.
15422         */
15423        this.setColourMap = function(map) {
15424            colourMap = map;
15425            this.fireEvent({
15426                "type": "colour-change",
15427                "wc": this.getCurrentWindowLut().getWindowLevel().getCenter(),
15428                "ww": this.getCurrentWindowLut().getWindowLevel().getWidth()
15429            });
15430        };
15431
15432        /**
15433         * Get the current position.
15434         * @return {Object} The current position.
15435         */
15436        this.getCurrentPosition = function() {
15437            // return a clone to avoid reference problems
15438            return { "i": currentPosition.i, "j": currentPosition.j, "k": currentPosition.k };
15439        };
15440        /**
15441         * Set the current position.
15442         * @param {Object} pos The current position.
15443         * @param {Boolean} silent If true, does not fire a slice-change event.
15444         * @return {Boolean} False if not in bounds
15445         */
15446        this.setCurrentPosition = function(pos, silent) {
15447            // default silent flag to false
15448            if (typeof silent === "undefined") {
15449                silent = false;
15450            }
15451            // check if possible
15452            if (!image.getGeometry().getSize().isInBounds(pos.i, pos.j, pos.k)) {
15453                return false;
15454            }
15455            var oldPosition = currentPosition;
15456            currentPosition = pos;
15457
15458            // fire a 'position-change' event
15459            if (image.getPhotometricInterpretation().match(/MONOCHROME/) !== null) {
15460                this.fireEvent({
15461                    "type": "position-change",
15462                    "i": pos.i,
15463                    "j": pos.j,
15464                    "k": pos.k,
15465                    "value": image.getRescaledValue(pos.i, pos.j, pos.k, this.getCurrentFrame())
15466                });
15467            } else {
15468                this.fireEvent({
15469                    "type": "position-change",
15470                    "i": pos.i,
15471                    "j": pos.j,
15472                    "k": pos.k
15473                });
15474            }
15475
15476            // fire a slice change event (used to trigger redraw)
15477            if (!silent) {
15478                if (oldPosition.k !== currentPosition.k) {
15479                    this.fireEvent({ "type": "slice-change" });
15480                }
15481            }
15482
15483            // all good
15484            return true;
15485        };
15486
15487        /**
15488         * Get the current frame number.
15489         * @return {Number} The current frame number.
15490         */
15491        this.getCurrentFrame = function() {
15492            return currentFrame;
15493        };
15494
15495        /**
15496         * Set the current frame number.
15497         * @param {Number} The current frame number.
15498         * @return {Boolean} False if not in bounds
15499         */
15500        this.setCurrentFrame = function(frame) {
15501            // check if possible
15502            if (frame < 0 || frame >= image.getNumberOfFrames()) {
15503                return false;
15504            }
15505            // assign
15506            var oldFrame = currentFrame;
15507            currentFrame = frame;
15508            // fire event
15509            if (oldFrame !== currentFrame && image.getNumberOfFrames() !== 1) {
15510                this.fireEvent({ "type": "frame-change", "frame": currentFrame });
15511                // silent set current position to update info text
15512                this.setCurrentPosition(this.getCurrentPosition(), true);
15513            }
15514            // all good
15515            return true;
15516        };
15517
15518        /**
15519         * Append another view to this one.
15520         * @param {Object} rhs The view to append.
15521         */
15522        this.append = function(rhs) {
15523            // append images
15524            var newSliceNumber = this.getImage().appendSlice(rhs.getImage());
15525            // update position if a slice was appended before
15526            if (newSliceNumber <= this.getCurrentPosition().k) {
15527                this.setCurrentPosition({
15528                    "i": this.getCurrentPosition().i,
15529                    "j": this.getCurrentPosition().j,
15530                    "k": this.getCurrentPosition().k + 1
15531                }, true);
15532            }
15533            // add window presets
15534            this.addWindowPresets(rhs.getWindowPresets(), newSliceNumber);
15535        };
15536
15537        /**
15538         * Append a frame buffer to the included image.
15539         * @param {Object} frameBuffer The frame buffer to append.
15540         */
15541        this.appendFrameBuffer = function(frameBuffer) {
15542            this.getImage().appendFrameBuffer(frameBuffer);
15543        };
15544
15545        /**
15546         * Set the view window/level.
15547         * @param {Number} center The window center.
15548         * @param {Number} width The window width.
15549         * @param {String} name Associated preset name, defaults to 'manual'.
15550         * Warning: uses the latest set rescale LUT or the default linear one.
15551         */
15552        this.setWindowLevel = function(center, width, name) {
15553            // window width shall be >= 1 (see https://www.dabsoft.ch/dicom/3/C.11.2.1.2/)
15554            if (width >= 1) {
15555
15556                // get current window/level (before updating name)
15557                var sliceNumber = this.getCurrentPosition().k;
15558                var currentWl = null;
15559                var rsi = image.getRescaleSlopeAndIntercept(sliceNumber);
15560                if (rsi && typeof rsi !== "undefined") {
15561                    var currentLut = windowLuts[rsi.toString()];
15562                    if (currentLut && typeof currentLut !== "undefined") {
15563                        currentWl = currentLut.getWindowLevel();
15564                    }
15565                }
15566
15567                if (typeof name === "undefined") {
15568                    name = "manual";
15569                }
15570                // update current preset name
15571                currentPresetName = name;
15572
15573                var wl = new dwv.image.WindowLevel(center, width);
15574                var keys = Object.keys(windowLuts);
15575
15576                // create the first lut if none exists
15577                if (keys.length === 0) {
15578                    // create the rescale lookup table
15579                    var rescaleLut = new dwv.image.lut.Rescale(
15580                        image.getRescaleSlopeAndIntercept(0), image.getMeta().BitsStored);
15581                    // create the window lookup table
15582                    var windowLut = new dwv.image.lut.Window(rescaleLut, image.getMeta().IsSigned);
15583                    this.addWindowLut(windowLut);
15584                }
15585
15586                // set window level on luts
15587                for (var key in windowLuts) {
15588                    windowLuts[key].setWindowLevel(wl);
15589                }
15590
15591                // fire window level change event
15592                if (currentWl && typeof currentWl !== "undefined") {
15593                    if (currentWl.getWidth() !== width) {
15594                        this.fireEvent({ "type": "wl-width-change", "wc": center, "ww": width });
15595                    }
15596                    if (currentWl.getCenter() !== center) {
15597                        this.fireEvent({ "type": "wl-center-change", "wc": center, "ww": width });
15598                    }
15599                } else {
15600                    this.fireEvent({ "type": "wl-width-change", "wc": center, "ww": width });
15601                    this.fireEvent({ "type": "wl-center-change", "wc": center, "ww": width });
15602                }
15603            }
15604        };
15605
15606        /**
15607         * Set the window level to the preset with the input name.
15608         * @param {String} name The name of the preset to activate.
15609         */
15610        this.setWindowLevelPreset = function(name) {
15611            var preset = this.getWindowPresets()[name];
15612            if (typeof preset === "undefined") {
15613                throw new Error("Unknown window level preset: '" + name + "'");
15614            }
15615            // special min/max
15616            if (name === "minmax" && typeof preset.wl === "undefined") {
15617                preset.wl = this.getWindowLevelMinMax();
15618            }
15619            // special 'perslice' case
15620            if (typeof preset.perslice !== "undefined" &&
15621                preset.perslice === true) {
15622                preset = { "wl": preset.wl[this.getCurrentPosition().k] };
15623            }
15624            // set w/l
15625            this.setWindowLevel(preset.wl.getCenter(), preset.wl.getWidth(), name);
15626        };
15627
15628        /**
15629         * Set the window level to the preset with the input id.
15630         * @param {Number} id The id of the preset to activate.
15631         */
15632        this.setWindowLevelPresetById = function(id) {
15633            var keys = Object.keys(this.getWindowPresets());
15634            this.setWindowLevelPreset(keys[id]);
15635        };
15636
15637        /**
15638         * Clone the image using all meta data and the original data buffer.
15639         * @return {View} A full copy of this {dwv.image.View}.
15640         */
15641        this.clone = function() {
15642            var copy = new dwv.image.View(this.getImage());
15643            for (var key in windowLuts) {
15644                copy.addWindowLut(windowLuts[key]);
15645            }
15646            copy.setListeners(this.getListeners());
15647            return copy;
15648        };
15649
15650        /**
15651         * View listeners
15652         * @private
15653         * @type Object
15654         */
15655        var listeners = {};
15656        /**
15657         * Get the view listeners.
15658         * @return {Object} The view listeners.
15659         */
15660        this.getListeners = function() { return listeners; };
15661        /**
15662         * Set the view listeners.
15663         * @param {Object} list The view listeners.
15664         */
15665        this.setListeners = function(list) { listeners = list; };
15666    };
15667
15668    /**
15669     * Get the image window/level that covers the full data range.
15670     * Warning: uses the latest set rescale LUT or the default linear one.
15671     */
15672    dwv.image.View.prototype.getWindowLevelMinMax = function() {
15673        var range = this.getImage().getRescaledDataRange();
15674        var min = range.min;
15675        var max = range.max;
15676        var width = max - min;
15677        var center = min + width / 2;
15678        return new dwv.image.WindowLevel(center, width);
15679    };
15680
15681    /**
15682     * Set the image window/level to cover the full data range.
15683     * Warning: uses the latest set rescale LUT or the default linear one.
15684     */
15685    dwv.image.View.prototype.setWindowLevelMinMax = function() {
15686        // calculate center and width
15687        var wl = this.getWindowLevelMinMax();
15688        // set window level
15689        this.setWindowLevel(wl.getCenter(), wl.getWidth(), "minmax");
15690    };
15691
15692    /**
15693     * Generate display image data to be given to a canvas.
15694     * @param {Array} array The array to fill in.
15695     */
15696    dwv.image.View.prototype.generateImageData = function(array) {
15697        var windowLut = this.getCurrentWindowLut();
15698
15699        var image = this.getImage();
15700        var sliceSize = image.getGeometry().getSize().getSliceSize();
15701        var sliceOffset = sliceSize * this.getCurrentPosition().k;
15702        var frame = (this.getCurrentFrame()) ? this.getCurrentFrame() : 0;
15703
15704        var index = 0;
15705        var pxValue = 0;
15706        var stepPos = 0;
15707
15708        var photoInterpretation = image.getPhotometricInterpretation();
15709        switch (photoInterpretation) {
15710            case "MONOCHROME1":
15711            case "MONOCHROME2":
15712                var colourMap = this.getColourMap();
15713                var iMax = sliceOffset + sliceSize;
15714                for (var i = sliceOffset; i < iMax; ++i) {
15715                    pxValue = parseInt(windowLut.getValue(
15716                        image.getValueAtOffset(i, frame)), 10);
15717                    array.data[index] = colourMap.red[pxValue];
15718                    array.data[index + 1] = colourMap.green[pxValue];
15719                    array.data[index + 2] = colourMap.blue[pxValue];
15720                    array.data[index + 3] = 0xff;
15721                    index += 4;
15722                }
15723                break;
15724
15725            case "RGB":
15726                // 3 times bigger...
15727                sliceOffset *= 3;
15728                // the planar configuration defines the memory layout
15729                var planarConfig = image.getPlanarConfiguration();
15730                if (planarConfig !== 0 && planarConfig !== 1) {
15731                    throw new Error("Unsupported planar configuration: " + planarConfig);
15732                }
15733                // default: RGBRGBRGBRGB...
15734                var posR = sliceOffset;
15735                var posG = sliceOffset + 1;
15736                var posB = sliceOffset + 2;
15737                stepPos = 3;
15738                // RRRR...GGGG...BBBB...
15739                if (planarConfig === 1) {
15740                    posR = sliceOffset;
15741                    posG = sliceOffset + sliceSize;
15742                    posB = sliceOffset + 2 * sliceSize;
15743                    stepPos = 1;
15744                }
15745
15746                for (var j = 0; j < sliceSize; ++j) {
15747                    array.data[index] = parseInt(windowLut.getValue(
15748                        image.getValueAtOffset(posR, frame)), 10);
15749                    array.data[index + 1] = parseInt(windowLut.getValue(
15750                        image.getValueAtOffset(posG, frame)), 10);
15751                    array.data[index + 2] = parseInt(windowLut.getValue(
15752                        image.getValueAtOffset(posB, frame)), 10);
15753                    array.data[index + 3] = 0xff;
15754                    index += 4;
15755
15756                    posR += stepPos;
15757                    posG += stepPos;
15758                    posB += stepPos;
15759                }
15760                break;
15761
15762            case "YBR_FULL_422":
15763                // theory:
15764                // http://dicom.nema.org/dicom/2013/output/chtml/part03/sect_C.7.html#sect_C.7.6.3.1.2
15765                // reverse equation:
15766                // https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
15767
15768                // 3 times bigger...
15769                sliceOffset *= 3;
15770                // the planar configuration defines the memory layout
15771                var planarConfigYBR = image.getPlanarConfiguration();
15772                if (planarConfigYBR !== 0 && planarConfigYBR !== 1) {
15773                    throw new Error("Unsupported planar configuration: " + planarConfigYBR);
15774                }
15775                // default: YBRYBRYBR...
15776                var posY = sliceOffset;
15777                var posCB = sliceOffset + 1;
15778                var posCR = sliceOffset + 2;
15779                stepPos = 3;
15780                // YYYY...BBBB...RRRR...
15781                if (planarConfigYBR === 1) {
15782                    posY = sliceOffset;
15783                    posCB = sliceOffset + sliceSize;
15784                    posCR = sliceOffset + 2 * sliceSize;
15785                    stepPos = 1;
15786                }
15787
15788                var y, cb, cr;
15789                var r, g, b;
15790                for (var k = 0; k < sliceSize; ++k) {
15791                    y = image.getValueAtOffset(posY, frame);
15792                    cb = image.getValueAtOffset(posCB, frame);
15793                    cr = image.getValueAtOffset(posCR, frame);
15794
15795                    r = y + 1.402 * (cr - 128);
15796                    g = y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128);
15797                    b = y + 1.772 * (cb - 128);
15798
15799                    array.data[index] = parseInt(windowLut.getValue(r), 10);
15800                    array.data[index + 1] = parseInt(windowLut.getValue(g), 10);
15801                    array.data[index + 2] = parseInt(windowLut.getValue(b), 10);
15802                    array.data[index + 3] = 0xff;
15803                    index += 4;
15804
15805                    posY += stepPos;
15806                    posCB += stepPos;
15807                    posCR += stepPos;
15808                }
15809                break;
15810
15811            default:
15812                throw new Error("Unsupported photometric interpretation: " + photoInterpretation);
15813        }
15814    };
15815
15816    /**
15817     * Add an event listener on the view.
15818     * @param {String} type The event type.
15819     * @param {Object} listener The method associated with the provided event type.
15820     */
15821    dwv.image.View.prototype.addEventListener = function(type, listener) {
15822        var listeners = this.getListeners();
15823        if (!listeners[type]) {
15824            listeners[type] = [];
15825        }
15826        listeners[type].push(listener);
15827    };
15828
15829    /**
15830     * Remove an event listener on the view.
15831     * @param {String} type The event type.
15832     * @param {Object} listener The method associated with the provided event type.
15833     */
15834    dwv.image.View.prototype.removeEventListener = function(type, listener) {
15835        var listeners = this.getListeners();
15836        if (!listeners[type]) {
15837            return;
15838        }
15839        for (var i = 0; i < listeners[type].length; ++i) {
15840            if (listeners[type][i] === listener) {
15841                listeners[type].splice(i, 1);
15842            }
15843        }
15844    };
15845
15846    /**
15847     * Fire an event: call all associated listeners.
15848     * @param {Object} event The event to fire.
15849     */
15850    dwv.image.View.prototype.fireEvent = function(event) {
15851        var listeners = this.getListeners();
15852        if (!listeners[event.type]) {
15853            return;
15854        }
15855        for (var i = 0; i < listeners[event.type].length; ++i) {
15856            listeners[event.type][i](event);
15857        }
15858    };
15859
15860    /**
15861     * View factory.
15862     * @constructor
15863     */
15864    dwv.image.ViewFactory = function() {};
15865
15866    /**
15867     * Get an View object from the read DICOM file.
15868     * @param {Object} dicomElements The DICOM tags.
15869     * @param {Object} image The associated image.
15870     * @return {View} The new View.
15871     */
15872    dwv.image.ViewFactory.prototype.create = function(dicomElements, image) {
15873        // view
15874        var view = new dwv.image.View(image);
15875
15876        // default color map
15877        if (image.getPhotometricInterpretation() === "MONOCHROME1") {
15878            view.setDefaultColourMap(dwv.image.lut.invPlain);
15879        }
15880
15881        // presets
15882        var windowPresets = {};
15883
15884        // DICOM presets
15885        var windowCenter = dicomElements.getFromKey("x00281050", true);
15886        var windowWidth = dicomElements.getFromKey("x00281051", true);
15887        var windowCWExplanation = dicomElements.getFromKey("x00281055", true);
15888        if (windowCenter && windowWidth) {
15889            var name;
15890            for (var j = 0; j < windowCenter.length; ++j) {
15891                var center = parseFloat(windowCenter[j], 10);
15892                var width = parseFloat(windowWidth[j], 10);
15893                if (center && width) {
15894                    name = "";
15895                    if (windowCWExplanation) {
15896                        name = dwv.dicom.cleanString(windowCWExplanation[j]);
15897                    }
15898                    if (name === "") {
15899                        name = "Default" + j;
15900                    }
15901                    windowPresets[name] = {
15902                        "wl": [new dwv.image.WindowLevel(center, width)],
15903                        "name": name,
15904                        "perslice": true
15905                    };
15906                }
15907            }
15908        }
15909
15910        // min/max
15911        // Not filled yet since it is stil too costly to calculate min/max
15912        // for each slice... It will be filled at first use (see view.setWindowLevelPreset).
15913        // Order is important, if no wl from DICOM, this will be the default.
15914        windowPresets.minmax = { "name": "minmax" };
15915
15916        // optional modality presets
15917        if (typeof dwv.tool.defaultpresets !== "undefined") {
15918            var modality = image.getMeta().Modality;
15919            for (var key in dwv.tool.defaultpresets[modality]) {
15920                var preset = dwv.tool.defaultpresets[modality][key];
15921                windowPresets[key] = {
15922                    "wl": new dwv.image.WindowLevel(preset.center, preset.width),
15923                    "name": key
15924                };
15925            }
15926        }
15927
15928        // store
15929        view.setWindowPresets(windowPresets);
15930
15931        return view;
15932    };
15933
15934    // namespaces
15935    var dwv = dwv || {};
15936    dwv.io = dwv.io || {};
15937
15938    /**
15939     * DICOM data loader.
15940     */
15941    dwv.io.DicomDataLoader = function() {
15942        // closure to self
15943        var self = this;
15944
15945        /**
15946         * Loader options.
15947         * @private
15948         * @type Object
15949         */
15950        var options = {};
15951
15952        /**
15953         * Loading flag.
15954         * @private
15955         * @type Boolean
15956         */
15957        var isLoading = false;
15958
15959        /**
15960         * Set the loader options.
15961         * @param {Object} opt The input options.
15962         */
15963        this.setOptions = function(opt) {
15964            options = opt;
15965        };
15966
15967        /**
15968         * Is the load ongoing?
15969         * @return {Boolean} True if loading.
15970         */
15971        this.isLoading = function() {
15972            return isLoading;
15973        };
15974
15975        /**
15976         * DICOM buffer to dwv.image.View (asynchronous)
15977         */
15978        var db2v =
15978 new dwv.image.DicomBufferToView();
15979
15980        /**
15981         * Load data.
15982         * @param {Object} buffer The DICOM buffer.
15983         * @param {String} origin The data origin.
15984         * @param {Number} index The data index.
15985         */
15986        this.load = function(buffer, origin, index) {
15987            // set loading flag
15988            isLoading = true;
15989            // set character set
15990            if (typeof options.defaultCharacterSet !== "undefined") {
15991                db2v.setDefaultCharacterSet(options.defaultCharacterSet);
15992            }
15993            // connect handlers
15994            db2v.onload = self.onload;
15995            db2v.onloadend = function() {
15996                // reset loading flag
15997                isLoading = false;
15998                // call listeners
15999                self.onloadend();
16000            };
16001            db2v.onprogress = self.onprogress;
16002            // convert
16003            try {
16004                db2v.convert(buffer, index);
16005            } catch (error) {
16006                // TODO: error will be for individual file, isLoading is global...
16007                //isLoading = false;
16008                self.onerror(error);
16009            }
16010        };
16011
16012        /**
16013         * Abort load.
16014         */
16015        this.abort = function() {
16016            // abort conversion
16017            db2v.abort();
16018            // reset loading flag
16019            isLoading = false;
16020            // call listeners
16021            self.onabort({ message: "Abort while loading DICOM data." });
16022        };
16023
16024        /**
16025         * Get a file load handler.
16026         * @param {Object} file The file to load.
16027         * @param {Number} index The index 'id' of the file.
16028         * @return {Function} A file load handler.
16029         */
16030        this.getFileLoadHandler = function(file, index) {
16031            return function(event) {
16032                self.load(event.target.result, file, index);
16033            };
16034        };
16035
16036        /**
16037         * Get a url load handler.
16038         * @param {String} url The url to load.
16039         * @param {Number} index The index 'id' of the url.
16040         * @return {Function} A url load handler.
16041         */
16042        this.getUrlLoadHandler = function(url, index) {
16043            return function( /*event*/ ) {
16044                // check response status
16045                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes
16046                // status 200: "OK"; status 0: "debug"
16047                if (this.status !== 200 && this.status !== 0) {
16048                    self.onerror({
16049                        'name': "RequestError",
16050                        'message': "Error status: " + this.status +
16051                            " while loading '" + url + "' [DicomDataLoader]"
16052                    });
16053                    return;
16054                }
16055                // load
16056                self.load(this.response, url, index);
16057            };
16058        };
16059
16060    }; // class DicomDataLoader
16061
16062    /**
16063     * Check if the loader can load the provided file.
16064     * @param {Object} file The file to check.
16065     * @return True if the file can be loaded.
16066     */
16067    dwv.io.DicomDataLoader.prototype.canLoadFile = function(file) {
16068        var split = file.name.split('.');
16069        var ext = "";
16070        if (split.length !== 1) {
16071            ext = split.pop().toLowerCase();
16072        }
16073        var hasExt = (ext.length !== 0);
16074        return !hasExt || (ext === "dcm");
16075    };
16076
16077    /**
16078     * Check if the loader can load the provided url.
16079     * @param {String} url The url to check.
16080     * @return True if the url can be loaded.
16081     */
16082    dwv.io.DicomDataLoader.prototype.canLoadUrl = function(url) {
16083        var split = url.split('.');
16084        var ext = "";
16085        if (split.length !== 1) {
16086            ext = split.pop().toLowerCase();
16087        }
16088        var hasExt = (ext.length !== 0) && (ext.length < 5);
16089        // wado url
16090        var isDicomContentType = (url.indexOf("contentType=application/dicom") !== -1);
16091
16092        return isDicomContentType || (ext === "dcm") || !hasExt;
16093    };
16094
16095    /**
16096     * Get the file content type needed by the loader.
16097     * @return One of the 'dwv.io.fileContentTypes'.
16098     */
16099    dwv.io.DicomDataLoader.prototype.loadFileAs = function() {
16100        return dwv.io.fileContentTypes.ArrayBuffer;
16101    };
16102
16103    /**
16104     * Get the url content type needed by the loader.
16105     * @return One of the 'dwv.io.urlContentTypes'.
16106     */
16107    dwv.io.DicomDataLoader.prototype.loadUrlAs = function() {
16108        return dwv.io.urlContentTypes.ArrayBuffer;
16109    };
16110
16111    /**
16112     * Handle a load event.
16113     * @param {Object} event The load event, 'event.target'
16114     *  should be the loaded data.
16115     * Default does nothing.
16116     */
16117    dwv.io.DicomDataLoader.prototype.onload = function( /*event*/ ) {};
16118    /**
16119     * Handle an load end event.
16120     * Default does nothing.
16121     */
16122    dwv.io.DicomDataLoader.prototype.onloadend = function() {};
16123    /**
16124     * Handle a progress event.
16125     * @param {Object} event The progress event.
16126     * Default does nothing.
16127     */
16128    dwv.io.DicomDataLoader.prototype.onprogress = function( /*event*/ ) {};
16129    /**
16130     * Handle an error event.
16131     * @param {Object} event The error event with an
16132     *  optional 'event.message'.
16133     * Default does nothing.
16134     */
16135    dwv.io.DicomDataLoader.prototype.onerror = function( /*event*/ ) {};
16136    /**
16137     * Handle an abort event.
16138     * @param {Object} event The abort event with an
16139     *  optional 'event.message'.
16140     * Default does nothing.
16141     */
16142    dwv.io.DicomDataLoader.prototype.onabort = function( /*event*/ ) {};
16143
16144    /**
16145     * Add to Loader list.
16146     */
16147    dwv.io.loaderList = dwv.io.loaderList || [];
16148    dwv.io.loaderList.push("DicomDataLoader");
16149
16150    // namespaces
16151    var dwv = dwv || {};
16152    /** @namespace */
16153    dwv.io = dwv.io || {};
16154
16155    // file content types
16156    dwv.io.fileContentTypes = {
16157        'Text': 0,
16158        'ArrayBuffer': 1,
16159        'DataURL': 2
16160    };
16161
16162    /**
16163     * Files loader.
16164     * @constructor
16165     */
16166    dwv.io.FilesLoader = function() {
16167        /**
16168         * Closure to self.
16169         * @private
16170         * @type Object
16171         */
16172        var self = this;
16173
16174        /**
16175         * Array of launched readers (used in abort).
16176         * @private
16177         * @type Array
16178         */
16179        var readers = [];
16180
16181        /**
16182         * Launched loader (used in abort).
16183         * @private
16184         * @type Object
16185         */
16186        var runningLoader = [];
16187
16188        /**
16189         * Number of data to load.
16190         * @private
16191         * @type Number
16192         */
16193        var nToLoad = 0;
16194        /**
16195         * Number of loaded data.
16196         * @private
16197         * @type Number
16198         */
16199        var nLoaded = 0;
16200
16201        /**
16202         * The default character set (optional).
16203         * @private
16204         * @type String
16205         */
16206        var defaultCharacterSet;
16207
16208        /**
16209         * Get the default character set.
16210         * @return {String} The default character set.
16211         */
16212        this.getDefaultCharacterSet = function() {
16213            return defaultCharacterSet;
16214        };
16215
16216        /**
16217         * Set the default character set.
16218         * @param {String} characterSet The character set.
16219         */
16220        this.setDefaultCharacterSet = function(characterSet) {
16221            defaultCharacterSet = characterSet;
16222        };
16223
16224        /**
16225         * Store a launched reader.
16226         * @param {Object} request The launched reader.
16227         */
16228        this.storeReader = function(reader) {
16229            readers.push(reader);
16230        };
16231
16232        /**
16233         * Clear the stored readers.
16234         */
16235        this.clearStoredReaders = function() {
16236            readers = [];
16237        };
16238
16239        /**
16240         * Store the launched loader.
16241         * @param {Object} loader The launched loader.
16242         */
16243        this.storeLoader = function(loader) {
16244            runningLoader = loader;
16245        };
16246
16247        /**
16248         * Clear the stored loader.
16249         */
16250        this.clearStoredLoader = function() {
16251            runningLoader = null;
16252        };
16253
16254        /**
16255         * Abort a URLs load.
16256         */
16257        this.abort = function() {
16258            // abort readers
16259            for (var i = 0; i < readers.length; ++i) {
16260                // 0: EMPTY, 1: LOADING, 2: DONE
16261                if (readers[i].readyState === 1) {
16262                    readers[i].abort();
16263                }
16264            }
16265            this.clearStoredReaders();
16266            // abort loader
16267            if (runningLoader) {
16268                runningLoader.abort();
16269            }
16270            this.clearStoredLoader();
16271        };
16272
16273        /**
16274         * Set the number of data to load.
16275         * @param {Number} n The number of data to load.
16276         */
16277        this.setNToLoad = function(n) {
16278            nToLoad = n;
16279        };
16280
16281        /**
16282         * Increment the number of loaded data
16283         * and call onloadend if loaded all data.
16284         */
16285        this.addLoaded = function() {
16286            nLoaded++;
16287            if (nLoaded === nToLoad) {
16288                self.onloadend();
16289            }
16290        };
16291
16292    }; // class File
16293
16294    /**
16295     * Handle a load event.
16296     * @param {Object} event The load event, 'event.target'
16297     *  should be the loaded data.
16298     * Default does nothing.
16299     */
16300    dwv.io.FilesLoader.prototype.onload = function( /*event*/ ) {};
16301    /**
16302     * Handle a load end event.
16303     * Default does nothing.
16304     */
16305    dwv.io.FilesLoader.prototype.onloadend = function() {};
16306    /**
16307     * Handle a progress event.
16308     * @param {Object} event The progress event.
16309     * Default does nothing.
16310     */
16311    dwv.io.FilesLoader.prototype.onprogress = function( /*event*/ ) {};
16312    /**
16313     * Handle an error event.
16314     * @param {Object} event The error event with an
16315     *  optional 'event.message'.
16316     * Default does nothing.
16317     */
16318    dwv.io.FilesLoader.prototype.onerror = function( /*event*/ ) {};
16319    /**
16320     * Handle an abort event.
16321     * @param {Object} event The abort event with an
16322     *  optional 'event.message'.
16323     * Default does nothing.
16324     */
16325    dwv.io.FilesLoader.prototype.onabort = function( /*event*/ ) {};
16326
16327    /**
16328     * Load a list of files.
16329     * @param {Array} ioArray The list of files to load.
16330     * @external FileReader
16331     */
16332    dwv.io.FilesLoader.prototype.load = function(ioArray) {
16333        // clear storage
16334        this.clearStoredReaders();
16335        this.clearStoredLoader();
16336
16337        // closure to self for handlers
16338        var self = this;
16339        // set the number of data to load
16340        this.setNToLoad(ioArray.length);
16341
16342        var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress);
16343        mproghandler.setNToLoad(ioArray.length);
16344
16345        // get loaders
16346        var loaders = [];
16347        for (var m = 0; m < dwv.io.loaderList.length; ++m) {
16348            loaders.push(new dwv.io[dwv.io.loaderList[m]]());
16349        }
16350
16351        // set loaders callbacks
16352        var loader = null;
16353        for (var k = 0; k < loaders.length; ++k) {
16354            loader = loaders[k];
16355            loader.onload = self.onload;
16356            loader.onloadend = self.addLoaded;
16357            loader.onerror = self.onerror;
16358            loader.onabort = self.onabort;
16359            loader.setOptions({
16360                'defaultCharacterSet': this.getDefaultCharacterSet()
16361            });
16362            loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1);
16363        }
16364
16365        // request onerror handler
16366        var getReaderOnError = function(origin) {
16367            return function(event) {
16368                var message = "An error occurred while reading '" + origin + "'";
16369                if (typeof event.getMessage !== "undefined") {
16370                    message += " (" + event.getMessage() + ")";
16371                }
16372                message += ".";
16373                self.onerror({ 'name': "FileReaderError", 'message': message });
16374            };
16375        };
16376
16377        // request onabort handler
16378        var getReaderOnAbort = function(origin) {
16379            return function() {
16380                self.onabort({ 'message': "Abort while reading '" + origin + "'" });
16381            };
16382        };
16383
16384        // loop on I/O elements
16385        for (var i = 0; i < ioArray.length; ++i) {
16386            var file = ioArray[i];
16387            var reader = new FileReader();
16388
16389            // store reader
16390            this.storeReader(reader);
16391
16392            // bind reader progress
16393            reader.onprogress = mproghandler.getMonoProgressHandler(i, 0);
16394
16395            // find a loader
16396            var foundLoader = false;
16397            for (var l = 0; l < loaders.length; ++l) {
16398                loader = loaders[l];
16399                if (loader.canLoadFile(file)) {
16400                    foundLoader = true;
16401                    // store loader
16402                    this.storeLoader(loader);
16403                    // set reader callbacks
16404                    reader.onload = loader.getFileLoadHandler(file, i);
16405                    reader.onerror = getReaderOnError(file.name);
16406                    reader.onabort = getReaderOnAbort(file.name);
16407                    // read
16408                    if (loader.loadFileAs() === dwv.io.fileContentTypes.Text) {
16409                        reader.readAsText(file);
16410                    } else if (loader.loadFileAs() === dwv.io.fileContentTypes.DataURL) {
16411                        reader.readAsDataURL(file);
16412                    } else if (loader.loadFileAs() === dwv.io.fileContentTypes.ArrayBuffer) {
16413                        reader.readAsArrayBuffer(file);
16414                    }
16415                    // next file
16416                    break;
16417                }
16418            }
16419            // TODO: throw?
16420            if (!foundLoader) {
16421                throw new Error("No loader found for file: " + file);
16422            }
16423        }
16424    };
16425
16426    // namespaces
16427    var dwv = dwv || {};
16428    dwv.io = dwv.io || {};
16429
16430    /**
16431     * JSON text loader.
16432     */
16433    dwv.io.JSONTextLoader = function() {
16434        // closure to self
16435        var self = this;
16436
16437        /**
16438         * Loading flag.
16439         * @private
16440         * @type Boolean
16441         */
16442        var isLoading = false;
16443
16444        /**
16445         * Set the loader options.
16446         * @param {Object} opt The input options.
16447         */
16448        this.setOptions = function() {
16449            // does nothing
16450        };
16451
16452        /**
16453         * Is the load ongoing?
16454         * @return {Boolean} True if loading.
16455         */
16456        this.isLoading = function() {
16457            return isLoading;
16458        };
16459
16460        /**
16461         * Load data.
16462         * @param {Object} text The input text.
16463         * @param {String} origin The data origin.
16464         * @param {Number} index The data index.
16465         */
16466        this.load = function(text, origin, index) {
16467            // set loading flag
16468            isLoading = true;
16469            try {
16470                self.onload(text);
16471                // reset loading flag
16472                isLoading = false;
16473                // call listeners
16474                self.onloadend();
16475            } catch (error) {
16476                self.onerror(error);
16477            }
16478            self.onprogress({
16479                'type': 'read-progress',
16480                'lengthComputable': true,
16481                'loaded': 100,
16482                'total': 100,
16483                'index': index
16484            });
16485        };
16486
16487        /**
16488         * Abort load: pass to listeners.
16489         */
16490        this.abort = function() {
16491            // reset loading flag
16492            isLoading = false;
16493            // call listeners
16494            self.onabort();
16495        };
16496
16497        /**
16498         * Get a file load handler.
16499         * @param {Object} file The file to load.
16500         * @param {Number} index The index 'id' of the file.
16501         * @return {Function} A file load handler.
16502         */
16503        this.getFileLoadHandler = function(file, index) {
16504            return function(event) {
16505                self.load(event.target.result, file, index);
16506            };
16507        };
16508
16509        /**
16510         * Get a url load handler.
16511         * @param {String} url The url to load.
16512         * @param {Number} index The index 'id' of the url.
16513         * @return {Function} A url load handler.
16514         */
16515        this.getUrlLoadHandler = function(url, index) {
16516            return function( /*event*/ ) {
16517                // check response status
16518                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes
16519                // status 200: "OK"; status 0: "debug"
16520                if (this.status !== 200 && this.status !== 0) {
16521                    self.onerror({
16522                        'name': "RequestError",
16523                        'message': "Error status: " + this.status +
16524                            " while loading '" + url + "' [JSONTextLoader]"
16525                    });
16526                    return;
16527                }
16528                // load
16529                self.load(this.responseText, url, index);
16530            };
16531        };
16532
16533    }; // class JSONTextLoader
16534
16535    /**
16536     * Check if the loader can load the provided file.
16537     * @param {Object} file The file to check.
16538     * @return True if the file can be loaded.
16539     */
16540    dwv.io.JSONTextLoader.prototype.canLoadFile = function(file) {
16541        var ext = file.name.split('.').pop().toLowerCase();
16542        return (ext === "json");
16543    };
16544
16545    /**
16546     * Check if the loader can load the provided url.
16547     * @param {String} url The url to check.
16548     * @return True if the url can be loaded.
16549     */
16550    dwv.io.JSONTextLoader.prototype.canLoadUrl = function(url) {
16551        var ext = url.split('.').pop().toLowerCase();
16552        return (ext === "json");
16553    };
16554
16555    /**
16556     * Get the file content type needed by the loader.
16557     * @return One of the 'dwv.io.fileContentTypes'.
16558     */
16559    dwv.io.JSONTextLoader.prototype.loadFileAs = function() {
16560        return dwv.io.fileContentTypes.Text;
16561    };
16562
16563    /**
16564     * Get the url content type needed by the loader.
16565     * @return One of the 'dwv.io.urlContentTypes'.
16566     */
16567    dwv.io.JSONTextLoader.prototype.loadUrlAs = function() {
16568        return dwv.io.urlContentTypes.Text;
16569    };
16570
16571    /**
16572     * Handle a load event.
16573     * @param {Object} event The load event, 'event.target'
16574     *  should be the loaded data.
16575     * Default does nothing.
16576     */
16577    dwv.io.JSONTextLoader.prototype.onload = function( /*event*/ ) {};
16578    /**
16579     * Handle an load end event.
16580     * Default does nothing.
16581     */
16582    dwv.io.JSONTextLoader.prototype.onloadend = function() {};
16583    /**
16584     * Handle a progress event.
16585     * @param {Object} event The progress event.
16586     * Default does nothing.
16587     */
16588    dwv.io.JSONTextLoader.prototype.onprogress = function( /*event*/ ) {};
16589    /**
16590     * Handle an error event.
16591     * @param {Object} event The error event with an
16592     *  optional 'event.message'.
16593     * Default does nothing.
16594     */
16595    dwv.io.JSONTextLoader.prototype.onerror = function( /*event*/ ) {};
16596    /**
16597     * Handle an abort event.
16598     * @param {Object} event The abort event with an
16599     *  optional 'event.message'.
16600     * Default does nothing.
16601     */
16602    dwv.io.JSONTextLoader.prototype.onabort = function( /*event*/ ) {};
16603
16604    /**
16605     * Add to Loader list.
16606     */
16607    dwv.io.loaderList = dwv.io.loaderList || [];
16608    dwv.io.loaderList.push("JSONTextLoader");
16609
16610    // namespaces
16611    var dwv = dwv || {};
16612    dwv.io = dwv.io || {};
16613
16614    /**
16615     * Memory loader.
16616     * @constructor
16617     */
16618    dwv.io.MemoryLoader = function() {
16619        /**
16620         * Closure to self.
16621         * @private
16622         * @type Object
16623         */
16624        var self = this;
16625
16626        /**
16627         * Launched loader (used in abort).
16628         * @private
16629         * @type Object
16630         */
16631        var runningLoader = null;
16632
16633        /**
16634         * Number of data to load.
16635         * @private
16636         * @type Number
16637         */
16638        var nToLoad = 0;
16639        /**
16640         * Number of loaded data.
16641         * @private
16642         * @type Number
16643         */
16644        var nLoaded = 0;
16645
16646        /**
16647         * The default character set (optional).
16648         * @private
16649         * @type String
16650         */
16651        var defaultCharacterSet;
16652
16653        /**
16654         * Get the default character set.
16655         * @return {String} The default character set.
16656         */
16657        this.getDefaultCharacterSet = function() {
16658            return defaultCharacterSet;
16659        };
16660
16661        /**
16662         * Set the default character set.
16663         * @param {String} characterSet The character set.
16664         */
16665        this.setDefaultCharacterSet = function(characterSet) {
16666            defaultCharacterSet = characterSet;
16667        };
16668
16669        /**
16670         * Store a launched loader.
16671         * @param {Object} loader The launched loader.
16672         */
16673        this.storeLoader = function(loader) {
16674            runningLoader = loader;
16675        };
16676
16677        /**
16678         * Clear the stored loader.
16679         */
16680        this.clearStoredLoader = function() {
16681            runningLoader = null;
16682        };
16683
16684        /**
16685         * Abort a memory load.
16686         */
16687        this.abort = function() {
16688            // abort loader
16689            runningLoader.abort();
16690            this.clearStoredLoaders();
16691        };
16692
16693        /**
16694         * Set the number of data to load.
16695         * @param {Number} n The number of data to load.
16696         */
16697        this.setNToLoad = function(n) {
16698            nToLoad = n;
16699        };
16700
16701        /**
16702         * Increment the number of loaded data
16703         * and call onloadend if loaded all data.
16704         */
16705        this.addLoaded = function() {
16706            nLoaded++;
16707            if (nLoaded === nToLoad) {
16708                self.onloadend();
16709            }
16710        };
16711
16712    }; // class Memory
16713
16714    /**
16715     * Handle a load event.
16716     * @param {Object} event The load event, 'event.target'
16717     *  should be the loaded data.
16718     * Default does nothing.
16719     */
16720    dwv.io.MemoryLoader.prototype.onload = function( /*event*/ ) {};
16721    /**
16722     * Handle a load end event.
16723     * Default does nothing.
16724     */
16725    dwv.io.MemoryLoader.prototype.onloadend = function() {};
16726    /**
16727     * Handle a progress event.
16728     * @param {Object} event The progress event.
16729     * Default does nothing.
16730     */
16731    dwv.io.MemoryLoader.prototype.onprogress = function( /*event*/ ) {};
16732    /**
16733     * Handle an error event.
16734     * @param {Object} event The error event with an
16735     *  optional 'event.message'.
16736     * Default does nothing.
16737     */
16738    dwv.io.MemoryLoader.prototype.onerror = function( /*event*/ ) {};
16739    /**
16740     * Handle an abort event.
16741     * @param {Object} event The abort event with an
16742     *  optional 'event.message'.
16743     * Default does nothing.
16744     */
16745    dwv.io.MemoryLoader.prototype.onabort = function( /*event*/ ) {};
16746
16747    /**
16748     * Load a list of buffers.
16749     * @param {Array} ioArray The list of buffers to load.
16750     */
16751    dwv.io.MemoryLoader.prototype.load = function(ioArray) {
16752        // clear storage
16753        this.clearStoredLoader();
16754
16755        // closure to self for handlers
16756        var self = this;
16757        // set the number of data to load
16758        this.setNToLoad(ioArray.length);
16759
16760        var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress);
16761        mproghandler.setNToLoad(ioArray.length);
16762
16763        // get loaders
16764        var loaders = [];
16765        for (var m = 0; m < dwv.io.loaderList.length; ++m) {
16766            loaders.push(new dwv.io[dwv.io.loaderList[m]]());
16767        }
16768
16769        // set loaders callbacks
16770        var loader = null;
16771        for (var k = 0; k < loaders.length; ++k) {
16772            loader = loaders[k];
16773            loader.onload = self.onload;
16774            loader.onloadend = self.addLoaded;
16775            loader.onerror = self.onerror;
16776            loader.onabort = self.onabort;
16777            loader.setOptions({
16778                'defaultCharacterSet': this.getDefaultCharacterSet()
16779            });
16780            loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1);
16781        }
16782
16783        // loop on I/O elements
16784        for (var i = 0; i < ioArray.length; ++i) {
16785            var iodata = ioArray[i];
16786
16787            // find a loader
16788            var foundLoader = false;
16789            for (var l = 0; l < loaders.length; ++l) {
16790                loader = loaders[l];
16791                if (loader.canLoadUrl(iodata.filename)) {
16792                    foundLoader = true;
16793                    // store loader
16794                    this.storeLoader(loader);
16795                    // read
16796                    loader.load(iodata.data, iodata.filename, i);
16797                    // next file
16798                    break;
16799                }
16800            }
16801            // TODO: throw?
16802            if (!foundLoader) {
16803                throw new Error("No loader found for file: " + iodata.filename);
16804            }
16805        }
16806    };
16807
16808    // namespaces
16809    var dwv = dwv || {};
16810    dwv.io = dwv.io || {};
16811
16812    /**
16813     * Raw image loader.
16814     */
16815    dwv.io.RawImageLoader = function() {
16816        // closure to self
16817        var self = this;
16818
16819        /**
16820         * Set the loader options.
16821         * @param {Object} opt The input options.
16822         */
16823        this.setOptions = function() {
16824            // does nothing
16825        };
16826
16827        /**
16828         * Is the load ongoing? TODO...
16829         * @return {Boolean} True if loading.
16830         */
16831        this.isLoading = function() {
16832            return true;
16833        };
16834
16835        /**
16836         * Create a Data URI from an HTTP request response.
16837         * @param {Object} response The HTTP request response.
16838         * @param {String} dataType The data type.
16839         */
16840        function createDataUri(response, dataType) {
16841            // image data as string
16842            var bytes = new Uint8Array(response);
16843            var imageDataStr = '';
16844            for (var i = 0; i < bytes.byteLength; ++i) {
16845                imageDataStr += String.fromCharCode(bytes[i]);
16846            }
16847            // image type
16848            var imageType = dataType;
16849            if (imageType === "jpg") {
16850                imageType = "jpeg";
16851            }
16852            // create uri
16853            var uri = "data:image/" + imageType + ";base64," + window.btoa(imageDataStr);
16854            return uri;
16855        }
16856
16857        /**
16858         * Load data.
16859         * @param {Object} dataUri The data URI.
16860         * @param {String} origin The data origin.
16861         * @param {Number} index The data index.
16862         */
16863        this.load = function(dataUri, origin, index) {
16864            // create a DOM image
16865            var image = new Image();
16866            image.src = dataUri;
16867            // storing values to pass them on
16868            image.origin = origin;
16869            image.index = index;
16870            // triggered by ctx.drawImage
16871            image.onload = function( /*event*/ ) {
16872                try {
16873                    self.onload(dwv.image.getViewFromDOMImage(this));
16874                    self.onloadend();
16875                } catch (error) {
16876                    self.onerror(error);
16877                }
16878                self.onprogress({
16879                    'type': 'read-progress',
16880                    'lengthComputable': true,
16881                    'loaded': 100,
16882                    'total': 100,
16883                    'index': index
16884                });
16885            };
16886        };
16887
16888        /**
16889         * Abort load. TODO...
16890         */
16891        this.abort = function() {
16892            self.onabort();
16893        };
16894
16895        /**
16896         * Get a file load handler.
16897         * @param {Object} file The file to load.
16898         * @param {Number} index The index 'id' of the file.
16899         * @return {Function} A file load handler.
16900         */
16901        this.getFileLoadHandler = function(file, index) {
16902            return function(event) {
16903                self.load(event.target.result, file, index);
16904            };
16905        };
16906
16907        /**
16908         * Get a url load handler.
16909         * @param {String} url The url to load.
16910         * @param {Number} index The index 'id' of the url.
16911         * @return {Function} A url load handler.
16912         */
16913        this.getUrlLoadHandler = function(url, index) {
16914            return function( /*event*/ ) {
16915                // check response status
16916                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes
16917                // status 200: "OK"; status 0: "debug"
16918                if (this.status !== 200 && this.status !== 0) {
16919                    self.onerror({
16920                        'name': "RequestError",
16921                        'message': "Error status: " + this.status +
16922                            " while loading '" + url + "' [RawImageLoader]"
16923                    });
16924                    return;
16925                }
16926                // load
16927                var ext = url.split('.').pop().toLowerCase();
16928                self.load(createDataUri(this.response, ext), url, index);
16929            };
16930        };
16931
16932    }; // class RawImageLoader
16933
16934    /**
16935     * Check if the loader can load the provided file.
16936     * @param {Object} file The file to check.
16937     * @return True if the file can be loaded.
16938     */
16939    dwv.io.RawImageLoader.prototype.canLoadFile = function(file) {
16940        return file.type.match("image.*");
16941    };
16942
16943    /**
16944     * Check if the loader can load the provided url.
16945     * @param {String} url The url to check.
16946     * @return True if the url can be loaded.
16947     */
16948    dwv.io.RawImageLoader.prototype.canLoadUrl = function(url) {
16949        var ext = url.split('.').pop().toLowerCase();
16950        var hasImageExt = (ext === "jpeg") || (ext === "jpg") ||
16951            (ext === "png") || (ext === "gif");
16952        // wado url
16953        var isImageContentType = (url.indexOf("contentType=image/jpeg") !== -1) ||
16954            (url.indexOf("contentType=image/png") !== -1) ||
16955            (url.indexOf("contentType=image/gif") !== -1);
16956
16957        return isImageContentType || hasImageExt;
16958    };
16959
16960    /**
16961     * Get the file content type needed by the loader.
16962     * @return One of the 'dwv.io.fileContentTypes'.
16963     */
16964    dwv.io.RawImageLoader.prototype.loadFileAs = function() {
16965        return dwv.io.fileContentTypes.DataURL;
16966    };
16967
16968    /**
16969     * Get the url content type needed by the loader.
16970     * @return One of the 'dwv.io.urlContentTypes'.
16971     */
16972    dwv.io.RawImageLoader.prototype.loadUrlAs = function() {
16973        return dwv.io.urlContentTypes.ArrayBuffer;
16974    };
16975
16976    /**
16977     * Handle a load event.
16978     * @param {Object} event The load event, 'event.target'
16979     *  should be the loaded data.
16980     * Default does nothing.
16981     */
16982    dwv.io.RawImageLoader.prototype.onload = function( /*event*/ ) {};
16983    /**
16984     * Handle an load end event.
16985     * Default does nothing.
16986     */
16987    dwv.io.RawImageLoader.prototype.onloadend = function() {};
16988    /**
16989     * Handle a progress event.
16990     * @param {Object} event The progress event.
16991     * Default does nothing.
16992     */
16993    dwv.io.RawImageLoader.prototype.onprogress = function( /*event*/ ) {};
16994    /**
16995     * Handle an error event.
16996     * @param {Object} event The error event with an
16997     *  optional 'event.message'.
16998     * Default does nothing.
16999     */
17000    dwv.io.RawImageLoader.prototype.onerror = function( /*event*/ ) {};
17001    /**
17002     * Handle an abort event.
17003     * @param {Object} event The abort event with an
17004     *  optional 'event.message'.
17005     * Default does nothing.
17006     */
17007    dwv.io.RawImageLoader.prototype.onabort = function( /*event*/ ) {};
17008
17009    /**
17010     * Add to Loader list.
17011     */
17012    dwv.io.loaderList = dwv.io.loaderList || [];
17013    dwv.io.loaderList.push("RawImageLoader");
17014
17015    // namespaces
17016    var dwv = dwv || {};
17017    dwv.io = dwv.io || {};
17018
17019    /**
17020     * Raw video loader.
17021     * url example (cors enabled):
17022     *   https://raw.githubusercontent.com/clappr/clappr/master/test/fixtures/SampleVideo_360x240_1mb.mp4
17023     */
17024    dwv.io.RawVideoLoader = function() {
17025        // closure to self
17026        var self = this;
17027
17028        /**
17029         * Set the loader options.
17030         * @param {Object} opt The input options.
17031         */
17032        this.setOptions = function() {
17033            // does nothing
17034        };
17035
17036        /**
17037         * Is the load ongoing? TODO...
17038         * @return {Boolean} True if loading.
17039         */
17040        this.isLoading = function() {
17041            return true;
17042        };
17043
17044        /**
17045         * Create a Data URI from an HTTP request response.
17046         * @param {Object} response The HTTP request response.
17047         * @param {String} dataType The data type.
17048         */
17049        function createDataUri(response, dataType) {
17050            // image data as string
17051            var bytes = new Uint8Array(response);
17052            var videoDataStr = '';
17053            for (var i = 0; i < bytes.byteLength; ++i) {
17054                videoDataStr += String.fromCharCode(bytes[i]);
17055            }
17056            // create uri
17057            var uri = "data:video/" + dataType + ";base64," + window.btoa(videoDataStr);
17058            return uri;
17059        }
17060
17061        /**
17062         * Internal Data URI load.
17063         * @param {Object} dataUri The data URI.
17064         * @param {String} origin The data origin.
17065         * @param {Number} index The data index.
17066         */
17067        this.load = function(dataUri, origin, index) {
17068            // create a DOM video
17069            var video = document.createElement('video');
17070            video.src = dataUri;
17071            // storing values to pass them on
17072            video.file = origin;
17073            video.index = index;
17074            // onload handler
17075            video.onloadedmetadata = function( /*event*/ ) {
17076                try {
17077                    dwv.image.getViewFromDOMVideo(this,
17078                        self.onload, self.onprogress, self.onloadend, index);
17079                } catch (error) {
17080                    self.onerror(error);
17081                }
17082            };
17083        };
17084
17085        /**
17086         * Abort load. TODO...
17087         */
17088        this.abort = function() {
17089            self.onabort();
17090        };
17091
17092        /**
17093         * Get a file load handler.
17094         * @param {Object} file The file to load.
17095         * @param {Number} index The index 'id' of the file.
17096         * @return {Function} A file load handler.
17097         */
17098        this.getFileLoadHandler = function(file, index) {
17099            return function(event) {
17100                self.load(event.target.result, file, index);
17101            };
17102        };
17103
17104        /**
17105         * Get a url load handler.
17106         * @param {String} url The url to load.
17107         * @param {Number} index The index 'id' of the url.
17108         * @return {Function} A url load handler.
17109         */
17110        this.getUrlLoadHandler = function(url, index) {
17111            return function( /*event*/ ) {
17112                // check response status
17113                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes
17114                // status 200: "OK"; status 0: "debug"
17115                if (this.status !== 200 && this.status !== 0) {
17116                    self.onerror({
17117                        'name': "RequestError",
17118                        'message': "Error status: " + this.status +
17119                            " while loading '" + url + "' [RawVideoLoader]"
17120                    });
17121                    return;
17122                }
17123                // load
17124                var ext = url.split('.').pop().toLowerCase();
17125                self.load(createDataUri(this.response, ext), url, index);
17126            };
17127        };
17128
17129    }; // class RawVideoLoader
17130
17131    /**
17132     * Check if the loader can load the provided file.
17133     * @param {Object} file The file to check.
17134     * @return True if the file can be loaded.
17135     */
17136    dwv.io.RawVideoLoader.prototype.canLoadFile = function(file) {
17137        return file.type.match("video.*");
17138    };
17139
17140    /**
17141     * Check if the loader can load the provided url.
17142     * @param {String} url The url to check.
17143     * @return True if the url can be loaded.
17144     */
17145    dwv.io.RawVideoLoader.prototype.canLoadUrl = function(url) {
17146        var ext = url.split('.').pop().toLowerCase();
17147        return (ext === "mp4") || (ext === "ogg") ||
17148            (ext === "webm");
17149    };
17150
17151    /**
17152     * Get the file content type needed by the loader.
17153     * @return One of the 'dwv.io.fileContentTypes'.
17154     */
17155    dwv.io.RawVideoLoader.prototype.loadFileAs = function() {
17156        return dwv.io.fileContentTypes.DataURL;
17157    };
17158
17159    /**
17160     * Get the url content type needed by the loader.
17161     * @return One of the 'dwv.io.urlContentTypes'.
17162     */
17163    dwv.io.RawVideoLoader.prototype.loadUrlAs = function() {
17164        return dwv.io.urlContentTypes.ArrayBuffer;
17165    };
17166
17167    /**
17168     * Handle a load event.
17169     * @param {Object} event The load event, 'event.target'
17170     *  should be the loaded data.
17171     * Default does nothing.
17172     */
17173    dwv.io.RawVideoLoader.prototype.onload = function( /*event*/ ) {};
17174    /**
17175     * Handle an load end event.
17176     * Default does nothing.
17177     */
17178    dwv.io.RawVideoLoader.prototype.onloadend = function() {};
17179    /**
17180     * Handle a progress event.
17181     * @param {Object} event The progress event.
17182     * Default does nothing.
17183     */
17184    dwv.io.RawVideoLoader.prototype.onprogress = function( /*event*/ ) {};
17185    /**
17186     * Handle an error event.
17187     * @param {Object} event The error event with an
17188     *  optional 'event.message'.
17189     * Default does nothing.
17190     */
17191    dwv.io.RawVideoLoader.prototype.onerror = function( /*event*/ ) {};
17192    /**
17193     * Handle an abort event.
17194     * @param {Object} event The abort event with an
17195     *  optional 'event.message'.
17196     * Default does nothing.
17197     */
17198    dwv.io.RawVideoLoader.prototype.onabort = function( /*event*/ ) {};
17199
17200    /**
17201     * Add to Loader list.
17202     */
17203    dwv.io.loaderList = dwv.io.loaderList || [];
17204    dwv.io.loaderList.push("RawVideoLoader");
17205
17206    // namespaces
17207    var dwv = dwv || {};
17208    dwv.io = dwv.io || {};
17209
17210    // url content types
17211    dwv.io.urlContentTypes = {
17212        'Text': 0,
17213        'ArrayBuffer': 1,
17214        'oups': 2
17215    };
17216
17217    /**
17218     * Urls loader.
17219     * @constructor
17220     */
17221    dwv.io.UrlsLoader = function() {
17222        /**
17223         * Closure to self.
17224         * @private
17225         * @type Object
17226         */
17227        var self = this;
17228
17229        /**
17230         * Array of launched requests used in abort.
17231         * @private
17232         * @type Array
17233         */
17234        var requests = [];
17235
17236        /**
17237         * Launched loader used in abort.
17238         * @private
17239         * @type Object
17240         */
17241        var runningLoader = null;
17242
17243        /**
17244         * Number of data to load.
17245         * @private
17246         * @type Number
17247         */
17248        var nToLoad = 0;
17249        /**
17250         * Number of loaded data.
17251         * @private
17252         * @type Number
17253         */
17254        var nLoaded = 0;
17255
17256        /**
17257         * The default character set (optional).
17258         * @private
17259         * @type String
17260         */
17261        var defaultCharacterSet;
17262
17263        /**
17264         * Get the default character set.
17265         * @return {String} The default character set.
17266         */
17267        this.getDefaultCharacterSet = function() {
17268            return defaultCharacterSet;
17269        };
17270
17271        /**
17272         * Set the default character set.
17273         * @param {String} characterSet The character set.
17274         */
17275        this.setDefaultCharacterSet = function(characterSet) {
17276            defaultCharacterSet = characterSet;
17277        };
17278
17279        /**
17280         * Store a launched request.
17281         * @param {Object} request The launched request.
17282         */
17283        this.storeRequest = function(request) {
17284            requests.push(request);
17285        };
17286
17287        /**
17288         * Clear the stored requests.
17289         */
17290        this.clearStoredRequests = function() {
17291            requests = [];
17292        };
17293
17294        /**
17295         * Store a launched loader.
17296         * @param {Object} loader The launched loader.
17297         */
17298        this.storeLoader = function(loader) {
17299            runningLoader = loader;
17300        };
17301
17302        /**
17303         * Clear the stored loader.
17304         */
17305        this.clearStoredLoader = function() {
17306            runningLoader = null;
17307        };
17308
17309        /**
17310         * Abort a URLs load.
17311         */
17312        this.abort = function() {
17313            // abort requests
17314            for (var i = 0; i < requests.length; ++i) {
17315                // 0: UNSENT, 1: OPENED, 2: HEADERS_RECEIVED (send()), 3: LOADING, 4: DONE
17316                if (requests[i].readyState === 2 || requests[i].readyState === 3) {
17317                    requests[i].abort();
17318                }
17319            }
17320            this.clearStoredRequests();
17321            // abort loader
17322            if (runningLoader && runningLoader.isLoading()) {
17323                runningLoader.abort();
17324            }
17325            this.clearStoredLoader();
17326        };
17327
17328        /**
17329         * Set the number of data to load.
17330         * @param {Number} n The number of data to load.
17331         */
17332        this.setNToLoad = function(n) {
17333            nToLoad = n;
17334        };
17335
17336        /**
17337         * Increment the number of loaded data
17338         * and call onloadend if loaded all data.
17339         */
17340        this.addLoaded = function() {
17341            nLoaded++;
17342            if (nLoaded === nToLoad) {
17343                self.onloadend();
17344            }
17345        };
17346
17347    }; // class Url
17348
17349    /**
17350     * Handle a load event.
17351     * @param {Object} event The load event, 'event.target'
17352     *  should be the loaded data.
17353     * Default does nothing.
17354     */
17355    dwv.io.UrlsLoader.prototype.onload = function( /*event*/ ) {};
17356    /**
17357     * Handle a load end event.
17358     * Default does nothing.
17359     */
17360    dwv.io.UrlsLoader.prototype.onloadend = function() {};
17361    /**
17362     * Handle a progress event.
17363     * @param {Object} event The progress event.
17364     * Default does nothing.
17365     */
17366    dwv.io.UrlsLoader.prototype.onprogress = function( /*event*/ ) {};
17367    /**
17368     * Handle an error event.
17369     * @param {Object} event The error event with an
17370     *  optional 'event.message'.
17371     * Default does nothing.
17372     */
17373    dwv.io.UrlsLoader.prototype.onerror = function( /*event*/ ) {};
17374    /**
17375     * Handle an abort event.
17376     * @param {Object} event The abort event with an
17377     *  optional 'event.message'.
17378     * Default does nothing.
17379     */
17380    dwv.io.UrlsLoader.prototype.onabort = function( /*event*/ ) {};
17381
17382    /**
17383     * Load a list of URLs.
17384     * @param {Array} ioArray The list of urls to load.
17385     * @param {Object} options Load options.
17386     * @external XMLHttpRequest
17387     */
17388    dwv.io.UrlsLoader.prototype.load = function(ioArray, options) {
17389        // clear storage
17390        this.clearStoredRequests();
17391        this.clearStoredLoader();
17392
17393        // closure to self for handlers
17394        var self = this;
17395        // set the number of data to load
17396        this.setNToLoad(ioArray.length);
17397
17398        var mproghandler = new dwv.utils.MultiProgressHandler(self.onprogress);
17399        mproghandler.setNToLoad(ioArray.length);
17400
17401        // get loaders
17402        var loaders = [];
17403        for (var m = 0; m < dwv.io.loaderList.length; ++m) {
17404            loaders.push(new dwv.io[dwv.io.loaderList[m]]());
17405        }
17406
17407        // set loaders callbacks
17408        var loader = null;
17409        for (var k = 0; k < loaders.length; ++k) {
17410            loader = loaders[k];
17411            loader.onload = self.onload;
17412            loader.onloadend = self.addLoaded;
17413            loader.onerror = self.onerror;
17414            loader.onabort = self.onabort;
17415            loader.setOptions({
17416                'defaultCharacterSet': this.getDefaultCharacterSet()
17417            });
17418            loader.onprogress = mproghandler.getUndefinedMonoProgressHandler(1);
17419        }
17420
17421        // request onerror handler
17422        var getRequestOnError = function(origin) {
17423            return function( /*event*/ ) {
17424                var message = "An error occurred while downloading '" + origin + "'";
17425                if (typeof this.status !== "undefined") {
17426                    message += " (http status: " + this.status + ")";
17427                }
17428                message += ".";
17429                self.onerror({ 'name': "RequestError", 'message': message });
17430            };
17431        };
17432
17433        // request onabort handler
17434        var getRequestOnAbort = function(origin) {
17435            return function() {
17436                self.onabort({ 'message': "Abort while downloading '" + origin + "'." });
17437            };
17438        };
17439
17440        // loop on I/O elements
17441        for (var i = 0; i < ioArray.length; ++i) {
17442            var url = ioArray[i];
17443            var request = new XMLHttpRequest();
17444            request.open('GET', url, true);
17445
17446            // store request
17447            this.storeRequest(request);
17448
17449            // optional request headers
17450            if (typeof options.requestHeaders !== "undefined") {
17451                var requestHeaders = options.requestHeaders;
17452                for (var j = 0; j < requestHeaders.length; ++j) {
17453                    if (typeof requestHeaders[j].name !== "undefined" &&
17454                        typeof requestHeaders[j].value !== "undefined") {
17455                        request.setRequestHeader(requestHeaders[j].name, requestHeaders[j].value);
17456                    }
17457                }
17458            }
17459
17460            // bind reader progress
17461            request.onprogress = mproghandler.getMonoProgressHandler(i, 0);
17462            request.onloadend = mproghandler.getMonoOnLoadEndHandler(i, 0);
17463
17464            // find a loader
17465            var foundLoader = false;
17466            for (var l = 0; l < loaders.length; ++l) {
17467                loader = loaders[l];
17468                if (loader.canLoadUrl(url)) {
17469                    foundLoader = true;
17470                    // store loader
17471                    this.storeLoader(loader);
17472                    // set reader callbacks
17473                    request.onload = loader.getUrlLoadHandler(url, i);
17474                    request.onerror = getRequestOnError(url);
17475                    request.onabort = getRequestOnAbort(url);
17476                    // response type (default is 'text')
17477                    if (loader.loadUrlAs() === dwv.io.urlContentTypes.ArrayBuffer) {
17478                        request.responseType = "arraybuffer";
17479                    }
17480                    // read
17481                    request.send(null);
17482                    // next file
17483                    break;
17484                }
17485            }
17486            // TODO: throw?
17487            if (!foundLoader) {
17488                throw new Error("No loader found for url: " + url);
17489            }
17490        }
17491    };
17492
17493    // namespaces
17494    var dwv = dwv || {};
17495    dwv.io = dwv.io || {};
17496    // external
17497    var JSZip = JSZip || {};
17498
17499    /**
17500     * ZIP data loader.
17501     */
17502    dwv.io.ZipLoader = function() {
17503        // closure to self
17504        var self = this;
17505
17506        /**
17507         * Loader options.
17508         * @private
17509         * @type Object
17510         */
17511        var options = {};
17512
17513        /**
17514         * Loading flag.
17515         * @private
17516         * @type Boolean
17517         */
17518        var isLoading = false;
17519
17520        /**
17521         * Set the loader options.
17522         * @param {Object} opt The input options.
17523         */
17524        this.setOptions = function(opt) {
17525            options = opt;
17526        };
17527
17528        /**
17529         * Is the load ongoing?
17530         * @return {Boolean} True if loading.
17531         */
17532        this.isLoading = function() {
17533            return isLoading;
17534        };
17535
17536        var filename = "";
17537        var files = [];
17538        var zobjs = null;
17539
17540        /**
17541         * JSZip.async callback
17542         * @param {ArrayBuffer} content unzipped file image
17543         * @return {}
17544         */
17545        function zipAsyncCallback(content) {
17546            files.push({ "filename": filename, "data": content });
17547
17548            if (files.length < zobjs.length) {
17549                var num = files.length;
17550                filename = zobjs[num].name;
17551                zobjs[num].async("arrayBuffer").then(zipAsyncCallback);
17552            } else {
17553                var memoryIO = new dwv.io.MemoryLoader();
17554                memoryIO.onload = self.onload;
17555                memoryIO.onloadend = function() {
17556                    // reset loading flag
17557                    isLoading = false;
17558                    // call listeners
17559                    self.onloadend();
17560                };
17561                memoryIO.onprogress = self.onprogress;
17562                memoryIO.onerror = self.onerror;
17563                memoryIO.onabort = self.onabort;
17564
17565                memoryIO.load(files);
17566            }
17567        }
17568
17569        /**
17570         * Load data.
17571         * @param {Object} buffer The DICOM buffer.
17572         * @param {String} origin The data origin.
17573         * @param {Number} index The data index.
17574         */
17575        this.load = function(buffer /*, origin, index*/ ) {
17576            // set loading flag
17577            isLoading = true;
17578
17579            JSZip.loadAsync(buffer).then(function(zip) {
17580                files = [];
17581                zobjs = zip.file(/.*\.dcm/);
17582                // recursively load zip files into the files array
17583                var num = files.length;
17584                filename = zobjs[num].name;
17585                zobjs[num].async("arrayBuffer").then(zipAsyncCallback);
17586            });
17587        };
17588
17589        /**
17590         * Abort load: pass to listeners.
17591         */
17592        this.abort = function() {
17593            // reset loading flag
17594            isLoading = false;
17595            // call listeners
17596            self.onabort();
17597        };
17598
17599        /**
17600         * Get a file load handler.
17601         * @param {Object} file The file to load.
17602         * @param {Number} index The index 'id' of the file.
17603         * @return {Function} A file load handler.
17604         */
17605        this.getFileLoadHandler = function(file, index) {
17606            return function(event) {
17607                self.load(event.target.result, file, index);
17608            };
17609        };
17610
17611        /**
17612         * Get a url load handler.
17613         * @param {String} url The url to load.
17614         * @param {Number} index The index 'id' of the url.
17615         * @return {Function} A url load handler.
17616         */
17617        this.getUrlLoadHandler = function(url, index) {
17618            return function( /*event*/ ) {
17619                // check response status
17620                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes
17621                // status 200: "OK"; status 0: "debug"
17622                if (this.status !== 200 && this.status !== 0) {
17623                    self.onerror({
17624                        'name': "RequestError",
17625                        'message': "Error status: " + this.status +
17626                            " while loading '" + url + "' [ZipLoader]"
17627                    });
17628                    return;
17629                }
17630                // load
17631                self.load(this.response, url, index);
17632            };
17633        };
17634
17635    }; // class DicomDataLoader
17636
17637    /**
17638     * Check if the loader can load the provided file.
17639     * @param {Object} file The file to check.
17640     * @return True if the file can be loaded.
17641     */
17642    dwv.io.ZipLoader.prototype.canLoadFile = function(file) {
17643        var ext = file.name.split('.').pop().toLowerCase();
17644        return (ext === "zip");
17645    };
17646
17647    /**
17648     * Check if the loader can load the provided url.
17649     * @param {String} url The url to check.
17650     * @return True if the url can be loaded.
17651     */
17652    dwv.io.ZipLoader.prototype.canLoadUrl = function(url) {
17653        var ext = url.split('.').pop().toLowerCase();
17654        return (ext === "zip");
17655    };
17656
17657    /**
17658     * Get the file content type needed by the loader.
17659     * @return One of the 'dwv.io.fileContentTypes'.
17660     */
17661    dwv.io.ZipLoader.prototype.loadFileAs = function() {
17662        return dwv.io.fileContentTypes.ArrayBuffer;
17663    };
17664
17665    /**
17666     * Get the url content type needed by the loader.
17667     * @return One of the 'dwv.io.urlContentTypes'.
17668     */
17669    dwv.io.ZipLoader.prototype.loadUrlAs = function() {
17670        return dwv.io.urlContentTypes.ArrayBuffer;
17671    };
17672
17673    /**
17674     * Handle a load event.
17675     * @param {Object} event The load event, 'event.target'
17676     *  should be the loaded data.
17677     * Default does nothing.
17678     */
17679    dwv.io.ZipLoader.prototype.onload = function( /*event*/ ) {};
17680    /**
17681     * Handle an load end event.
17682     * Default does nothing.
17683     */
17684    dwv.io.ZipLoader.prototype.onloadend = function() {};
17685    /**
17686     * Handle a progress event.
17687     * @param {Object} event The progress event.
17688     * Default does nothing.
17689     */
17690    dwv.io.ZipLoader.prototype.onprogress = function( /*event*/ ) {};
17691    /**
17692     * Handle an error event.
17693     * @param {Object} event The error event with an
17694     *  optional 'event.message'.
17695     * Default does nothing.
17696     */
17697    dwv.io.ZipLoader.prototype.onerror = function( /*event*/ ) {};
17698    /**
17699     * Handle an abort event.
17700     * @param {Object} event The abort event with an
17701     *  optional 'event.message'.
17702     * Default does nothing.
17703     */
17704    dwv.io.ZipLoader.prototype.onabort = function( /*event*/ ) {};
17705
17706    /**
17707     * Add to Loader list.
17708     */
17709    dwv.io.loaderList = dwv.io.loaderList || [];
17710    dwv.io.loaderList.push("ZipLoader");
17711
17712    // namespaces
17713    var dwv = dwv || {};
17714    /** @namespace */
17715    dwv.math = dwv.math || {};
17716
17717    /**
17718     * Circular Bucket Queue.
17719     *
17720     * Returns input'd points in sorted order. All operations run in roughly O(1)
17721     * time (for input with small cost values), but it has a strict requirement:
17722     *
17723     * If the most recent point had a cost of c, any points added should have a cost
17724     * c' in the range c <= c' <= c + (capacity - 1).
17725     *
17726     * @constructor
17727     * @input bits
17728     * @input cost_functor
17729     */
17730    dwv.math.BucketQueue = function(bits, cost_functor) {
17731        this.bucketCount = 1 << bits; // # of buckets = 2^bits
17732        this.mask = this.bucketCount - 1; // 2^bits - 1 = index mask
17733        this.size = 0;
17734
17735        this.loc = 0; // Current index in bucket list
17736
17737        // Cost defaults to item value
17738        this.cost = (typeof(cost_functor) !== 'undefined') ? cost_functor : function(item) {
17739            return item;
17740        };
17741
17742        this.buckets = this.buildArray(this.bucketCount);
17743    };
17744
17745    dwv.math.BucketQueue.prototype.push = function(item) {
17746        // Prepend item to the list in the appropriate bucket
17747        var bucket = this.getBucket(item);
17748        item.next = this.buckets[bucket];
17749        this.buckets[bucket] = item;
17750
17751        this.size++;
17752    };
17753
17754    dwv.math.BucketQueue.prototype.pop = function() {
17755        if (this.size === 0) {
17756            throw new Error("Cannot pop, bucketQueue is empty.");
17757        }
17758
17759        // Find first empty bucket
17760        while (this.buckets[this.loc] === null) {
17761            this.loc = (this.loc + 1) % this.bucketCount;
17762        }
17763
17764        // All items in bucket have same cost, return the first one
17765        var ret = this.buckets[this.loc];
17766        this.buckets[this.loc] = ret.next;
17767        ret.next = null;
17768
17769        this.size--;
17770        return ret;
17771    };
17772
17773    dwv.math.BucketQueue.prototype.remove = function(item) {
17774        // Tries to remove item from queue. Returns true on success, false otherwise
17775        if (!item) {
17776            return false;
17777        }
17778
17779        // To find node, go to bucket and search through unsorted list.
17780        var bucket = this.getBucket(item);
17781        var node = this.buckets[bucket];
17782
17783        while (node !== null && !item.equals(node.next)) {
17784            node = node.next;
17785        }
17786
17787        if (node === null) {
17788            // Item not in list, ergo item not in queue
17789            return false;
17790        } else {
17791            // Found item, do standard list node deletion
17792            node.next = node.next.next;
17793
17794            this.size--;
17795            return true;
17796        }
17797    };
17798
17799    dwv.math.BucketQueue.prototype.isEmpty = function() {
17800        return this.size === 0;
17801    };
17802
17803    dwv.math.BucketQueue.prototype.getBucket = function(item) {
17804        // Bucket index is the masked cost
17805        return this.cost(item) & this.mask;
17806    };
17807
17808    dwv.math.BucketQueue.prototype.buildArray = function(newSize) {
17809        // Create array and initialze pointers to null
17810        var buckets = new Array(newSize);
17811
17812        for (var i = 0; i < buckets.length; i++) {
17813            buckets[i] = null;
17814        }
17815
17816        return buckets;
17817    };
17818
17819    // namespaces
17820    var dwv = dwv || {};
17821    dwv.math = dwv.math || {};
17822
17823    /**
17824     * Immutable 3x3 Matrix.
17825     * @constructor
17826     */
17827    dwv.math.Matrix33 = function(
17828        m00, m01, m02,
17829        m10, m11, m12,
17830        m20, m21, m22) {
17831        // row-major order
17832        var mat = new Float32Array(9);
17833        mat[0] = m00;
17834        mat[1] = m01;
17835        mat[2] = m02;
17836        mat[3] = m10;
17837        mat[4] = m11;
17838        mat[5] = m12;
17839        mat[6] = m20;
17840        mat[7] = m21;
17841        mat[8] = m22;
17842
17843        /**
17844         * Get a value of the matrix.
17845         * @param {Number} row The row at wich to get the value.
17846         * @param {Number} col The column at wich to get the value.
17847         */
17848        this.get = function(row, col) {
17849            return mat[row * 3 + col];
17850        };
17851    }; // Matrix33
17852
17853    /**
17854     * Check for Matrix33 equality.
17855     * @param {Object} rhs The other matrix to compare to.
17856     * @return {Boolean} True if both matrices are equal.
17857     */
17858    dwv.math.Matrix33.prototype.equals = function(rhs) {
17859        return this.get(0, 0) === rhs.get(0, 0) && this.get(0, 1) === rhs.get(0, 1) &&
17860            this.get(0, 2) === rhs.get(0, 2) && this.get(1, 0) === rhs.get(1, 0) &&
17861            this.get(1, 1) === rhs.get(1, 1) && this.get(1, 2) === rhs.get(1, 2) &&
17862            this.get(2, 0) === rhs.get(2, 0) && this.get(2, 1) === rhs.get(2, 1) &&
17863            this.get(2, 2) === rhs.get(2, 2);
17864    };
17865
17866    /**
17867     * Get a string representation of the Matrix33.
17868     * @return {String}
17868 The matrix as a string.
17869     */
17870    dwv.math.Matrix33.prototype.toString = function() {
17871        return "[" + this.get(0, 0) + ", " + this.get(0, 1) + ", " + this.get(0, 2) +
17872            "\n " + this.get(1, 0) + ", " + this.get(1, 1) + ", " + this.get(1, 2) +
17873            "\n " + this.get(2, 0) + ", " + this.get(2, 1) + ", " + this.get(2, 2) + "]";
17874    };
17875
17876    /**
17877     * Multiply this matrix by a 3D vector.
17878     * @param {Object} vector3D The input 3D vector
17879     * @return {Object} The result 3D vector
17880     */
17881    dwv.math.Matrix33.multiplyVector3D = function(vector3D) {
17882        // cache matrix values
17883        var m00 = this.get(0, 0);
17884        var m01 = this.get(0, 1);
17885        var m02 = this.get(0, 2);
17886        var m10 = this.get(1, 0);
17887        var m11 = this.get(1, 1);
17888        var m12 = this.get(1, 2);
17889        var m20 = this.get(2, 0);
17890        var m21 = this.get(2, 1);
17891        var m22 = this.get(2, 2);
17892        // cache vector values
17893        var vx = vector3D.getX();
17894        var vy = vector3D.getY();
17895        var vz = vector3D.getZ();
17896        // calculate
17897        return new dwv.math.Vector3D(
17898            (m00 * vx) + (m01 * vy) + (m02 * vz),
17899            (m10 * vx) + (m11 * vy) + (m12 * vz),
17900            (m20 * vx) + (m21 * vy) + (m22 * vz));
17901    };
17902
17903    /**
17904     * Create a 3x3 identity matrix.
17905     * @return {Object} The identity matrix.
17906     */
17907    dwv.math.getIdentityMat33 = function() {
17908        return new dwv.math.Matrix33(
17909            1, 0, 0,
17910            0, 1, 0,
17911            0, 0, 1);
17912    };
17913
17914    // namespaces
17915    var dwv = dwv || {};
17916    dwv.math = dwv.math || {};
17917
17918    /**
17919     * Immutable 2D point.
17920     * @constructor
17921     * @param {Number} x The X coordinate for the point.
17922     * @param {Number} y The Y coordinate for the point.
17923     */
17924    dwv.math.Point2D = function(x, y) {
17925        /**
17926         * Get the X position of the point.
17927         * @return {Number} The X position of the point.
17928         */
17929        this.getX = function() { return x; };
17930        /**
17931         * Get the Y position of the point.
17932         * @return {Number} The Y position of the point.
17933         */
17934        this.getY = function() { return y; };
17935    }; // Point2D class
17936
17937    /**
17938     * Check for Point2D equality.
17939     * @param {Object} rhs The other point to compare to.
17940     * @return {Boolean} True if both points are equal.
17941     */
17942    dwv.math.Point2D.prototype.equals = function(rhs) {
17943        return rhs !== null &&
17944            this.getX() === rhs.getX() &&
17945            this.getY() === rhs.getY();
17946    };
17947
17948    /**
17949     * Get a string representation of the Point2D.
17950     * @return {String} The point as a string.
17951     */
17952    dwv.math.Point2D.prototype.toString = function() {
17953        return "(" + this.getX() + ", " + this.getY() + ")";
17954    };
17955
17956    /**
17957     * Get the distance to another Point2D.
17958     * @param {Object} point2D The input point.
17959     */
17960    dwv.math.Point2D.prototype.getDistance = function(point2D) {
17961        return Math.sqrt(
17962            (this.getX() - point2D.getX()) * (this.getX() - point2D.getX()) +
17963            (this.getY() - point2D.getY()) * (this.getY() - point2D.getY()));
17964    };
17965
17966    /**
17967     * Mutable 2D point.
17968     * @constructor
17969     * @param {Number} x The X coordinate for the point.
17970     * @param {Number} y The Y coordinate for the point.
17971     */
17972    dwv.math.FastPoint2D = function(x, y) {
17973        this.x = x;
17974        this.y = y;
17975    }; // FastPoint2D class
17976
17977    /**
17978     * Check for FastPoint2D equality.
17979     * @param {Object} other The other point to compare to.
17980     * @return {Boolean} True if both points are equal.
17981     */
17982    dwv.math.FastPoint2D.prototype.equals = function(rhs) {
17983        return rhs !== null &&
17984            this.x === rhs.x &&
17985            this.y === rhs.y;
17986    };
17987
17988    /**
17989     * Get a string representation of the FastPoint2D.
17990     * @return {String} The point as a string.
17991     */
17992    dwv.math.FastPoint2D.prototype.toString = function() {
17993        return "(" + this.x + ", " + this.y + ")";
17994    };
17995
17996    /**
17997     * Immutable 3D point.
17998     * @constructor
17999     * @param {Number} x The X coordinate for the point.
18000     * @param {Number} y The Y coordinate for the point.
18001     * @param {Number} z The Z coordinate for the point.
18002     */
18003    dwv.math.Point3D = function(x, y, z) {
18004        /**
18005         * Get the X position of the point.
18006         * @return {Number} The X position of the point.
18007         */
18008        this.getX = function() { return x; };
18009        /**
18010         * Get the Y position of the point.
18011         * @return {Number} The Y position of the point.
18012         */
18013        this.getY = function() { return y; };
18014        /**
18015         * Get the Z position of the point.
18016         * @return {Number} The Z position of the point.
18017         */
18018        this.getZ = function() { return z; };
18019    }; // Point3D class
18020
18021    /**
18022     * Check for Point3D equality.
18023     * @param {Object} rhs The other point to compare to.
18024     * @return {Boolean} True if both points are equal.
18025     */
18026    dwv.math.Point3D.prototype.equals = function(rhs) {
18027        return rhs !== null &&
18028            this.getX() === rhs.getX() &&
18029            this.getY() === rhs.getY() &&
18030            this.getZ() === rhs.getZ();
18031    };
18032
18033    /**
18034     * Get a string representation of the Point3D.
18035     * @return {String} The point as a string.
18036     */
18037    dwv.math.Point3D.prototype.toString = function() {
18038        return "(" + this.getX() +
18039            ", " + this.getY() +
18040            ", " + this.getZ() + ")";
18041    };
18042
18043    /**
18044     * Get the distance to another Point3D.
18045     * @param {Object} point3D The input point.
18046     */
18047    dwv.math.Point3D.prototype.getDistance = function(point3D) {
18048        return Math.sqrt(
18049            (this.getX() - point3D.getX()) * (this.getX() - point3D.getX()) +
18050            (this.getY() - point3D.getY()) * (this.getY() - point3D.getY()) +
18051            (this.getZ() - point3D.getZ()) * (this.getZ() - point3D.getZ()));
18052    };
18053
18054    /**
18055     * Get the difference to another Point3D.
18056     * @param {Object} point3D The input point.
18057     * @return {Object} The 3D vector from the input point to this one.
18058     */
18059    dwv.math.Point3D.prototype.minus = function(point3D) {
18060        return new dwv.math.Vector3D(
18061            (this.getX() - point3D.getX()),
18062            (this.getY() - point3D.getY()),
18063            (this.getZ() - point3D.getZ()));
18064    };
18065
18066    /**
18067     * Immutable 3D index.
18068     * @constructor
18069     * @param {Number} i The column index.
18070     * @param {Number} j The row index.
18071     * @param {Number} k The slice index.
18072     */
18073    dwv.math.Index3D = function(i, j, k) {
18074        /**
18075         * Get the column index.
18076         * @return {Number} The column index.
18077         */
18078        this.getI = function() { return i; };
18079        /**
18080         * Get the row index.
18081         * @return {Number} The row index.
18082         */
18083        this.getJ = function() { return j; };
18084        /**
18085         * Get the slice index.
18086         * @return {Number} The slice index.
18087         */
18088        this.getK = function() { return k; };
18089    }; // Index3D class
18090
18091    /**
18092     * Check for Index3D equality.
18093     * @param {Object} rhs The other index to compare to.
18094     * @return {Boolean} True if both indices are equal.
18095     */
18096    dwv.math.Index3D.prototype.equals = function(rhs) {
18097        return rhs !== null &&
18098            this.getI() === rhs.getI() &&
18099            this.getJ() === rhs.getJ() &&
18100            this.getK() === rhs.getK();
18101    };
18102
18103    /**
18104     * Get a string representation of the Index3D.
18105     * @return {String} The Index3D as a string.
18106     */
18107    dwv.math.Index3D.prototype.toString = function() {
18108        return "(" + this.getI() +
18109            ", " + this.getJ() +
18110            ", " + this.getK() + ")";
18111    };
18112
18113    // namespaces
18114    var dwv = dwv || {};
18115    dwv.math = dwv.math || {};
18116
18117    // Pre-created to reduce allocation in inner loops
18118    var __twothirdpi = (2 / (3 * Math.PI));
18119
18120    /**
18121     *
18122     */
18123    dwv.math.computeGreyscale = function(data, width, height) {
18124        // Returns 2D augmented array containing greyscale data
18125        // Greyscale values found by averaging colour channels
18126        // Input should be in a flat RGBA array, with values between 0 and 255
18127        var greyscale = [];
18128
18129        // Compute actual values
18130        for (var y = 0; y < height; y++) {
18131            greyscale[y] = [];
18132
18133            for (var x = 0; x < width; x++) {
18134                var p = (y * width + x) * 4;
18135                greyscale[y][x] = (data[p] + data[p + 1] + data[p + 2]) / (3 * 255);
18136            }
18137        }
18138
18139        // Augment with convenience functions
18140        greyscale.dx = function(x, y) {
18141            if (x + 1 === this[y].length) {
18142                // If we're at the end, back up one
18143                x--;
18144            }
18145            return this[y][x + 1] - this[y][x];
18146        };
18147
18148        greyscale.dy = function(x, y) {
18149            if (y + 1 === this.length) {
18150                // If we're at the end, back up one
18151                y--;
18152            }
18153            return this[y][x] - this[y + 1][x];
18154        };
18155
18156        greyscale.gradMagnitude = function(x, y) {
18157            var dx = this.dx(x, y);
18158            var dy = this.dy(x, y);
18159            return Math.sqrt(dx * dx + dy * dy);
18160        };
18161
18162        greyscale.laplace = function(x, y) {
18163            // Laplacian of Gaussian
18164            var lap = -16 * this[y][x];
18165            lap += this[y - 2][x];
18166            lap += this[y - 1][x - 1] + 2 * this[y - 1][x] + this[y - 1][x + 1];
18167            lap += this[y][x - 2] + 2 * this[y][x - 1] + 2 * this[y][x + 1] + this[y][x + 2];
18168            lap += this[y + 1][x - 1] + 2 * this[y + 1][x] + this[y + 1][x + 1];
18169            lap += this[y + 2][x];
18170
18171            return lap;
18172        };
18173
18174        return greyscale;
18175    };
18176
18177    /**
18178     *
18179     */
18180    dwv.math.computeGradient = function(greyscale) {
18181        // Returns a 2D array of gradient magnitude values for greyscale. The values
18182        // are scaled between 0 and 1, and then flipped, so that it works as a cost
18183        // function.
18184        var gradient = [];
18185
18186        var max = 0; // Maximum gradient found, for scaling purposes
18187
18188        var x = 0;
18189        var y = 0;
18190
18191        for (y = 0; y < greyscale.length - 1; y++) {
18192            gradient[y] = [];
18193
18194            for (x = 0; x < greyscale[y].length - 1; x++) {
18195                gradient[y][x] = greyscale.gradMagnitude(x, y);
18196                max = Math.max(gradient[y][x], max);
18197            }
18198
18199            gradient[y][greyscale[y].length - 1] = gradient[y][greyscale.length - 2];
18200        }
18201
18202        gradient[greyscale.length - 1] = [];
18203        for (var i = 0; i < gradient[0].length; i++) {
18204            gradient[greyscale.length - 1][i] = gradient[greyscale.length - 2][i];
18205        }
18206
18207        // Flip and scale.
18208        for (y = 0; y < gradient.length; y++) {
18209            for (x = 0; x < gradient[y].length; x++) {
18210                gradient[y][x] = 1 - (gradient[y][x] / max);
18211            }
18212        }
18213
18214        return gradient;
18215    };
18216
18217    /**
18218     *
18219     */
18220    dwv.math.computeLaplace = function(greyscale) {
18221        // Returns a 2D array of Laplacian of Gaussian values
18222        var laplace = [];
18223
18224        // Make the edges low cost here.
18225
18226        laplace[0] = [];
18227        laplace[1] = [];
18228        for (var i = 1; i < greyscale.length; i++) {
18229            // Pad top, since we can't compute Laplacian
18230            laplace[0][i] = 1;
18231            laplace[1][i] = 1;
18232        }
18233
18234        for (var y = 2; y < greyscale.length - 2; y++) {
18235            laplace[y] = [];
18236            // Pad left, ditto
18237            laplace[y][0] = 1;
18238            laplace[y][1] = 1;
18239
18240            for (var x = 2; x < greyscale[y].length - 2; x++) {
18241                // Threshold needed to get rid of clutter.
18242                laplace[y][x] = (greyscale.laplace(x, y) > 0.33) ? 0 : 1;
18243            }
18244
18245            // Pad right, ditto
18246            laplace[y][greyscale[y].length - 2] = 1;
18247            laplace[y][greyscale[y].length - 1] = 1;
18248        }
18249
18250        laplace[greyscale.length - 2] = [];
18251        laplace[greyscale.length - 1] = [];
18252        for (var j = 1; j < greyscale.length; j++) {
18253            // Pad bottom, ditto
18254            laplace[greyscale.length - 2][j] = 1;
18255            laplace[greyscale.length - 1][j] = 1;
18256        }
18257
18258        return laplace;
18259    };
18260
18261    dwv.math.computeGradX = function(greyscale) {
18262        // Returns 2D array of x-gradient values for greyscale
18263        var gradX = [];
18264
18265        for (var y = 0; y < greyscale.length; y++) {
18266            gradX[y] = [];
18267
18268            for (var x = 0; x < greyscale[y].length - 1; x++) {
18269                gradX[y][x] = greyscale.dx(x, y);
18270            }
18271
18272            gradX[y][greyscale[y].length - 1] = gradX[y][greyscale[y].length - 2];
18273        }
18274
18275        return gradX;
18276    };
18277
18278    dwv.math.computeGradY = function(greyscale) {
18279        // Returns 2D array of y-gradient values for greyscale
18280        var gradY = [];
18281
18282        for (var y = 0; y < greyscale.length - 1; y++) {
18283            gradY[y] = [];
18284
18285            for (var x = 0; x < greyscale[y].length; x++) {
18286                gradY[y][x] = greyscale.dy(x, y);
18287            }
18288        }
18289
18290        gradY[greyscale.length - 1] = [];
18291        for (var i = 0; i < greyscale[0].length; i++) {
18292            gradY[greyscale.length - 1][i] = gradY[greyscale.length - 2][i];
18293        }
18294
18295        return gradY;
18296    };
18297
18298    dwv.math.gradUnitVector = function(gradX, gradY, px, py, out) {
18299        // Returns the gradient vector at (px,py), scaled to a magnitude of 1
18300        var ox = gradX[py][px];
18301        var oy = gradY[py][px];
18302
18303        var gvm = Math.sqrt(ox * ox + oy * oy);
18304        gvm = Math.max(gvm, 1e-100); // To avoid possible divide-by-0 errors
18305
18306        out.x = ox / gvm;
18307        out.y = oy / gvm;
18308    };
18309
18310    dwv.math.gradDirection = function(gradX, gradY, px, py, qx, qy) {
18311        var __dgpuv = new dwv.math.FastPoint2D(-1, -1);
18312        var __gdquv = new dwv.math.FastPoint2D(-1, -1);
18313        // Compute the gradiant direction, in radians, between to points
18314        dwv.math.gradUnitVector(gradX, gradY, px, py, __dgpuv);
18315        dwv.math.gradUnitVector(gradX, gradY, qx, qy, __gdquv);
18316
18317        var dp = __dgpuv.y * (qx - px) - __dgpuv.x * (qy - py);
18318        var dq = __gdquv.y * (qx - px) - __gdquv.x * (qy - py);
18319
18320        // Make sure dp is positive, to keep things consistant
18321        if (dp < 0) {
18322            dp = -dp;
18323            dq = -dq;
18324        }
18325
18326        if (px !== qx && py !== qy) {
18327            // We're going diagonally between pixels
18328            dp *= Math.SQRT1_2;
18329            dq *= Math.SQRT1_2;
18330        }
18331
18332        return __twothirdpi * (Math.acos(dp) + Math.acos(dq));
18333    };
18334
18335    dwv.math.computeSides = function(dist, gradX, gradY, greyscale) {
18336        // Returns 2 2D arrays, containing inside and outside greyscale values.
18337        // These greyscale values are the intensity just a little bit along the
18338        // gradient vector, in either direction, from the supplied point. These
18339        // values are used when using active-learning Intelligent Scissors
18340
18341        var sides = {};
18342        sides.inside = [];
18343        sides.outside = [];
18344
18345        var guv = new dwv.math.FastPoint2D(-1, -1); // Current gradient unit vector
18346
18347        for (var y = 0; y < gradX.length; y++) {
18348            sides.inside[y] = [];
18349            sides.outside[y] = [];
18350
18351            for (var x = 0; x < gradX[y].length; x++) {
18352                dwv.math.gradUnitVector(gradX, gradY, x, y, guv);
18353
18354                //(x, y) rotated 90 = (y, -x)
18355
18356                var ix = Math.round(x + dist * guv.y);
18357                var iy = Math.round(y - dist * guv.x);
18358                var ox = Math.round(x - dist * guv.y);
18359                var oy = Math.round(y + dist * guv.x);
18360
18361                ix = Math.max(Math.min(ix, gradX[y].length - 1), 0);
18362                ox = Math.max(Math.min(ox, gradX[y].length - 1), 0);
18363                iy = Math.max(Math.min(iy, gradX.length - 1), 0);
18364                oy = Math.max(Math.min(oy, gradX.length - 1), 0);
18365
18366                sides.inside[y][x] = greyscale[iy][ix];
18367                sides.outside[y][x] = greyscale[oy][ox];
18368            }
18369        }
18370
18371        return sides;
18372    };
18373
18374    dwv.math.gaussianBlur = function(buffer, out) {
18375        // Smooth values over to fill in gaps in the mapping
18376        out[0] = 0.4 * buffer[0] + 0.5 * buffer[1] + 0.1 * buffer[1];
18377        out[1] = 0.25 * buffer[0] + 0.4 * buffer[1] + 0.25 * buffer[2] + 0.1 * buffer[3];
18378
18379        for (var i = 2; i < buffer.length - 2; i++) {
18380            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];
18381        }
18382
18383        var len = buffer.length;
18384        out[len - 2] = 0.25 * buffer[len - 1] + 0.4 * buffer[len - 2] + 0.25 * buffer[len - 3] + 0.1 * buffer[len - 4];
18385        out[len - 1] = 0.4 * buffer[len - 1] + 0.5 * buffer[len - 2] + 0.1 * buffer[len - 3];
18386    };
18387
18388
18389    /**
18390     * Scissors
18391     *
18392     * Ref: Eric N. Mortensen, William A. Barrett, Interactive Segmentation with
18393     *   Intelligent Scissors, Graphical Models and Image Processing, Volume 60,
18394     *   Issue 5, September 1998, Pages 349-384, ISSN 1077-3169,
18395     *   DOI: 10.1006/gmip.1998.0480.
18396     *
18397     * {@link http://www.sciencedirect.com/science/article/B6WG4-45JB8WN-9/2/6fe59d8089fd1892c2bfb82283065579}
18398     *
18399     * Highly inspired from {@link http://code.google.com/p/livewire-javascript/}
18400     * @constructor
18401     */
18402    dwv.math.Scissors = function() {
18403        this.width = -1;
18404        this.height = -1;
18405
18406        this.curPoint = null; // Corrent point we're searching on.
18407        this.searchGranBits = 8; // Bits of resolution for BucketQueue.
18408        this.searchGran = 1 << this.earchGranBits; //bits.
18409        this.pointsPerPost = 500;
18410
18411        // Precomputed image data. All in ranges 0 >= x >= 1 and all inverted (1 - x).
18412        this.greyscale = null; // Greyscale of image
18413        this.laplace = null; // Laplace zero-crossings (either 0 or 1).
18414        this.gradient = null; // Gradient magnitudes.
18415        this.gradX = null; // X-differences.
18416        this.gradY = null; // Y-differences.
18417
18418        this.parents = null; // Matrix mapping point => parent along shortest-path to root.
18419
18420        this.working = false; // Currently computing shortest paths?
18421
18422        // Begin Training:
18423        this.trained = false;
18424        this.trainingPoints = null;
18425
18426        this.edgeWidth = 2;
18427        this.trainingLength = 32;
18428
18429        this.edgeGran = 256;
18430        this.edgeTraining = null;
18431
18432        this.gradPointsNeeded = 32;
18433        this.gradGran = 1024;
18434        this.gradTraining = null;
18435
18436        this.insideGran = 256;
18437        this.insideTraining = null;
18438
18439        this.outsideGran = 256;
18440        this.outsideTraining = null;
18441        // End Training
18442    }; // Scissors class
18443
18444    // Begin training methods //
18445    dwv.math.Scissors.prototype.getTrainingIdx = function(granularity, value) {
18446        return Math.round((granularity - 1) * value);
18447    };
18448
18449    dwv.math.Scissors.prototype.getTrainedEdge = function(edge) {
18450        return this.edgeTraining[this.getTrainingIdx(this.edgeGran, edge)];
18451    };
18452
18453    dwv.math.Scissors.prototype.getTrainedGrad = function(grad) {
18454        return this.gradTraining[this.getTrainingIdx(this.gradGran, grad)];
18455    };
18456
18457    dwv.math.Scissors.prototype.getTrainedInside = function(inside) {
18458        return this.insideTraining[this.getTrainingIdx(this.insideGran, inside)];
18459    };
18460
18461    dwv.math.Scissors.prototype.getTrainedOutside = function(outside) {
18462        return this.outsideTraining[this.getTrainingIdx(this.outsideGran, outside)];
18463    };
18464    // End training methods //
18465
18466    dwv.math.Scissors.prototype.setWorking = function(working) {
18467        // Sets working flag
18468        this.working = working;
18469    };
18470
18471    dwv.math.Scissors.prototype.setDimensions = function(width, height) {
18472        this.width = width;
18473        this.height = height;
18474    };
18475
18476    dwv.math.Scissors.prototype.setData = function(data) {
18477        if (this.width === -1 || this.height === -1) {
18478            // The width and height should have already been set
18479            throw new Error("Dimensions have not been set.");
18480        }
18481
18482        this.greyscale = dwv.math.computeGreyscale(data, this.width, this.height);
18483        this.laplace = dwv.math.computeLaplace(this.greyscale);
18484        this.gradient = dwv.math.computeGradient(this.greyscale);
18485        this.gradX = dwv.math.computeGradX(this.greyscale);
18486        this.gradY = dwv.math.computeGradY(this.greyscale);
18487
18488        var sides = dwv.math.computeSides(this.edgeWidth, this.gradX, this.gradY, this.greyscale);
18489        this.inside = sides.inside;
18490        this.outside = sides.outside;
18491        this.edgeTraining = [];
18492        this.gradTraining = [];
18493        this.insideTraining = [];
18494        this.outsideTraining = [];
18495    };
18496
18497    dwv.math.Scissors.prototype.findTrainingPoints = function(p) {
18498        // Grab the last handful of points for training
18499        var points = [];
18500
18501        if (this.parents !== null) {
18502            for (var i = 0; i < this.trainingLength && p; i++) {
18503                points.push(p);
18504                p = this.parents[p.y][p.x];
18505            }
18506        }
18507
18508        return points;
18509    };
18510
18511    dwv.math.Scissors.prototype.resetTraining = function() {
18512        this.trained = false; // Training is ignored with this flag set
18513    };
18514
18515    dwv.math.Scissors.prototype.doTraining = function(p) {
18516        // Compute training weights and measures
18517        this.trainingPoints = this.findTrainingPoints(p);
18518
18519        if (this.trainingPoints.length < 8) {
18520            return; // Not enough points, I think. It might crash if length = 0.
18521        }
18522
18523        var buffer = [];
18524        this.calculateTraining(buffer, this.edgeGran, this.greyscale, this.edgeTraining);
18525        this.calculateTraining(buffer, this.gradGran, this.gradient, this.gradTraining);
18526        this.calculateTraining(buffer, this.insideGran, this.inside, this.insideTraining);
18527        this.calculateTraining(buffer, this.outsideGran, this.outside, this.outsideTraining);
18528
18529        if (this.trainingPoints.length < this.gradPointsNeeded) {
18530            // If we have two few training points, the gradient weight map might not
18531            // be smooth enough, so average with normal weights.
18532            this.addInStaticGrad(this.trainingPoints.length, this.gradPointsNeeded);
18533        }
18534
18535        this.trained = true;
18536    };
18537
18538    dwv.math.Scissors.prototype.calculateTraining = function(buffer, granularity, input, output) {
18539        var i = 0;
18540        // Build a map of raw-weights to trained-weights by favoring input values
18541        buffer.length = granularity;
18542        for (i = 0; i < granularity; i++) {
18543            buffer[i] = 0;
18544        }
18545
18546        var maxVal = 1;
18547        for (i = 0; i < this.trainingPoints.length; i++) {
18548            var p = this.trainingPoints[i];
18549            var idx = this.getTrainingIdx(granularity, input[p.y][p.x]);
18550            buffer[idx] += 1;
18551
18552            maxVal = Math.max(maxVal, buffer[idx]);
18553        }
18554
18555        // Invert and scale.
18556        for (i = 0; i < granularity; i++) {
18557            buffer[i] = 1 - buffer[i] / maxVal;
18558        }
18559
18560        // Blur it, as suggested. Gets rid of static.
18561        dwv.math.gaussianBlur(buffer, output);
18562    };
18563
18564    dwv.math.Scissors.prototype.addInStaticGrad = function(have, need) {
18565        // Average gradient raw-weights to trained-weights map with standard weight
18566        // map so that we don't end up with something to spiky
18567        for (var i = 0; i < this.gradGran; i++) {
18568            this.gradTraining[i] = Math.min(this.gradTraining[i], 1 - i * (need - have) / (need * this.gradGran));
18569        }
18570    };
18571
18572    dwv.math.Scissors.prototype.gradDirection = function(px, py, qx, qy) {
18573        return dwv.math.gradDirection(this.gradX, this.gradY, px, py, qx, qy);
18574    };
18575
18576    dwv.math.Scissors.prototype.dist = function(px, py, qx, qy) {
18577        // The grand culmunation of most of the code: the weighted distance function
18578        var grad = this.gradient[qy][qx];
18579
18580        if (px === qx || py === qy) {
18581            // The distance is Euclidean-ish; non-diagonal edges should be shorter
18582            grad *= Math.SQRT1_2;
18583        }
18584
18585        var lap = this.laplace[qy][qx];
18586        var dir = this.gradDirection(px, py, qx, qy);
18587
18588        if (this.trained) {
18589            // Apply training magic
18590            var gradT = this.getTrainedGrad(grad);
18591            var edgeT = this.getTrainedEdge(this.greyscale[py][px]);
18592            var insideT = this.getTrainedInside(this.inside[py][px]);
18593            var outsideT = this.getTrainedOutside(this.outside[py][px]);
18594
18595            return 0.3 * gradT + 0.3 * lap + 0.1 * (dir + edgeT + insideT + outsideT);
18596        } else {
18597            // Normal weights
18598            return 0.43 * grad + 0.43 * lap + 0.11 * dir;
18599        }
18600    };
18601
18602    dwv.math.Scissors.prototype.adj = function(p) {
18603        var list = [];
18604
18605        var sx = Math.max(p.x - 1, 0);
18606        var sy = Math.max(p.y - 1, 0);
18607        var ex = Math.min(p.x + 1, this.greyscale[0].length - 1);
18608        var ey = Math.min(p.y + 1, this.greyscale.length - 1);
18609
18610        var idx = 0;
18611        for (var y = sy; y <= ey; y++) {
18612            for (var x = sx; x <= ex; x++) {
18613                if (x !== p.x || y !== p.y) {
18614                    list[idx++] = new dwv.math.FastPoint2D(x, y);
18615                }
18616            }
18617        }
18618
18619        return list;
18620    };
18621
18622    dwv.math.Scissors.prototype.setPoint = function(sp) {
18623        this.setWorking(true);
18624
18625        this.curPoint = sp;
18626
18627        var x = 0;
18628        var y = 0;
18629
18630        this.visited = [];
18631        for (y = 0; y < this.height; y++) {
18632            this.visited[y] = [];
18633            for (x = 0; x < this.width; x++) {
18634                this.visited[y][x] = false;
18635            }
18636        }
18637
18638        this.parents = [];
18639        for (y = 0; y < this.height; y++) {
18640            this.parents[y] = [];
18641        }
18642
18643        this.cost = [];
18644        for (y = 0; y < this.height; y++) {
18645            this.cost[y] = [];
18646            for (x = 0; x < this.width; x++) {
18647                this.cost[y][x] = Number.MAX_VALUE;
18648            }
18649        }
18650
18651        this.pq = new dwv.math.BucketQueue(this.searchGranBits, function(p) {
18652            return Math.round(this.searchGran * this.costArr[p.y][p.x]);
18653        });
18654        this.pq.searchGran = this.searchGran;
18655        this.pq.costArr = this.cost;
18656
18657        this.pq.push(sp);
18658        this.cost[sp.y][sp.x] = 0;
18659    };
18660
18661    dwv.math.Scissors.prototype.doWork = function() {
18662        if (!this.working) {
18663            return;
18664        }
18665
18666        this.timeout = null;
18667
18668        var pointCount = 0;
18669        var newPoints = [];
18670        while (!this.pq.isEmpty() && pointCount < this.pointsPerPost) {
18671            var p = this.pq.pop();
18672            newPoints.push(p);
18673            newPoints.push(this.parents[p.y][p.x]);
18674
18675            this.visited[p.y][p.x] = true;
18676
18677            var adjList = this.adj(p);
18678            for (var i = 0; i < adjList.length; i++) {
18679                var q = adjList[i];
18680
18681                var pqCost = this.cost[p.y][p.x] + this.dist(p.x, p.y, q.x, q.y);
18682
18683                if (pqCost < this.cost[q.y][q.x]) {
18684                    if (this.cost[q.y][q.x] !== Number.MAX_VALUE) {
18685                        // Already in PQ, must remove it so we can re-add it.
18686                        this.pq.remove(q);
18687                    }
18688
18689                    this.cost[q.y][q.x] = pqCost;
18690                    this.parents[q.y][q.x] = p;
18691                    this.pq.push(q);
18692                }
18693            }
18694
18695            pointCount++;
18696        }
18697
18698        return newPoints;
18699    };
18700
18701    // namespaces
18702    var dwv = dwv || {};
18703    dwv.math = dwv.math || {};
18704
18705    /**
18706     * Mulitply the three inputs if the last two are not null.
18707     * @param {Number} a The first input.
18708     * @param {Number} b The second input.
18709     * @param {Number} c The third input.
18710     * @return {Number} The multiplication of the three inputs or
18711     *  null if one of the last two is null.
18712     */
18713    function mulABC(a, b, c) {
18714        var res = null;
18715        if (b !== null && c !== null) {
18716            res = a * b * c;
18717        }
18718        return res;
18719    }
18720
18721    /**
18722     * Circle shape.
18723     * @constructor
18724     * @param {Object} centre A Point2D representing the centre of the circle.
18725     * @param {Number} radius The radius of the circle.
18726     */
18727    dwv.math.Circle = function(centre, radius) {
18728        /**
18729         * Circle surface.
18730         * @private
18731         * @type Number
18732         */
18733        var surface = Math.PI * radius * radius;
18734
18735        /**
18736         * Get the centre (point) of the circle.
18737         * @return {Object} The center (point) of the circle.
18738         */
18739        this.getCenter = function() { return centre; };
18740        /**
18741         * Get the radius of the circle.
18742         * @return {Number} The radius of the circle.
18743         */
18744        this.getRadius = function() { return radius; };
18745        /**
18746         * Get the surface of the circle.
18747         * @return {Number} The surface of the circle.
18748         */
18749        this.getSurface = function() { return surface; };
18750        /**
18751         * Get the surface of the circle according to a spacing.
18752         * @param {Number} spacingX The X spacing.
18753         * @param {Number} spacingY The Y spacing.
18754         * @return {Number} The surface of the circle multiplied by the given
18755         *  spacing or null for null spacings.
18756         */
18757        this.getWorldSurface = function(spacingX, spacingY) {
18758            return mulABC(surface, spacingX, spacingY);
18759        };
18760    }; // Circle class
18761
18762    /**
18763     * Ellipse shape.
18764     * @constructor
18765     * @param {Object} centre A Point2D representing the centre of the ellipse.
18766     * @param {Number} a The radius of the ellipse on the horizontal axe.
18767     * @param {Number} b The radius of the ellipse on the vertical axe.
18768     */
18769    dwv.math.Ellipse = function(centre, a, b) {
18770        /**
18771         * Circle surface.
18772         * @private
18773         * @type Number
18774         */
18775        var surface = Math.PI * a * b;
18776
18777        /**
18778         * Get the centre (point) of the ellipse.
18779         * @return {Object} The center (point) of the ellipse.
18780         */
18781        this.getCenter = function() { return centre; };
18782        /**
18783         * Get the radius of the ellipse on the horizontal axe.
18784         * @return {Number} The radius of the ellipse on the horizontal axe.
18785         */
18786        this.getA = function() { return a; };
18787        /**
18788         * Get the radius of the ellipse on the vertical axe.
18789         * @return {Number} The radius of the ellipse on the vertical axe.
18790         */
18791        this.getB = function() { return b; };
18792        /**
18793         * Get the surface of the ellipse.
18794         * @return {Number} The surface of the ellipse.
18795         */
18796        this.getSurface = function() { return surface; };
18797        /**
18798         * Get the surface of the ellipse according to a spacing.
18799         * @param {Number} spacingX The X spacing.
18800         * @param {Number} spacingY The Y spacing.
18801         * @return {Number} The surface of the ellipse multiplied by the given
18802         *  spacing or null for null spacings.
18803         */
18804        this.getWorldSurface = function(spacingX, spacingY) {
18805            return mulABC(surface, spacingX, spacingY);
18806        };
18807    }; // Circle class
18808
18809    /**
18810     * Line shape.
18811     * @constructor
18812     * @param {Object} begin A Point2D representing the beginning of the line.
18813     * @param {Object} end A Point2D representing the end of the line.
18814     */
18815    dwv.math.Line = function(begin, end) {
18816        /**
18817         * Line delta in the X direction.
18818         * @private
18819         * @type Number
18820         */
18821        var dx = end.getX() - begin.getX();
18822        /**
18823         * Line delta in the Y direction.
18824         * @private
18825         * @type Number
18826         */
18827        var dy = end.getY() - begin.getY();
18828        /**
18829         * Line length.
18830         * @private
18831         * @type Number
18832         */
18833        var length = Math.sqrt(dx * dx + dy * dy);
18834
18835        /**
18836         * Get the begin point of the line.
18837         * @return {Object} The beginning point of the line.
18838         */
18839        this.getBegin = function() { return begin; };
18840        /**
18841         * Get the end point of the line.
18842         * @return {Object} The ending point of the line.
18843         */
18844        this.getEnd = function() { return end; };
18845        /**
18846         * Get the line delta in the X direction.
18847         * @return {Number} The delta in the X direction.
18848         */
18849        this.getDeltaX = function() { return dx; };
18850        /**
18851         * Get the line delta in the Y direction.
18852         * @return {Number} The delta in the Y direction.
18853         */
18854        this.getDeltaY = function() { return dy; };
18855        /**
18856         * Get the length of the line.
18857         * @return {Number} The length of the line.
18858         */
18859        this.getLength = function() { return length; };
18860        /**
18861         * Get the length of the line according to a  spacing.
18862         * @param {Number} spacingX The X spacing.
18863         * @param {Number} spacingY The Y spacing.
18864         * @return {Number} The length of the line with spacing
18865         *  or null for null spacings.
18866         */
18867        this.getWorldLength = function(spacingX, spacingY) {
18868            var wlen = null;
18869            if (spacingX !== null && spacingY !== null) {
18870                var dxs = dx * spacingX;
18871                var dys = dy * spacingY;
18872                wlen = Math.sqrt(dxs * dxs + dys * dys);
18873            }
18874            return wlen;
18875        };
18876        /**
18877         * Get the mid point of the line.
18878         * @return {Object} The mid point of the line.
18879         */
18880        this.getMidpoint = function() {
18881            return new dwv.math.Point2D(
18882                parseInt((begin.getX() + end.getX()) / 2, 10),
18883                parseInt((begin.getY() + end.getY()) / 2, 10));
18884        };
18885        /**
18886         * Get the slope of the line.
18887         * @return {Number} The slope of the line.
18888         */
18889        this.getSlope = function() {
18890            return dy / dx;
18891        };
18892        /**
18893         * Get the intercept of the line.
18894         * @return {Number} The slope of the line.
18895         */
18896        this.getIntercept = function() {
18897            return (end.getX() * begin.getY() - begin.getX() * end.getY()) / dx;
18898        };
18899        /**
18900         * Get the inclination of the line.
18901         * @return {Number} The inclination of the line.
18902         */
18903        this.getInclination = function() {
18904            // tan(theta) = slope
18905            var angle = Math.atan2(dy, dx) * 180 / Math.PI;
18906            // shift?
18907            return 180 - angle;
18908        };
18909    }; // Line class
18910
18911    /**
18912     * Get the angle between two lines.
18913     * @param line0 The first line.
18914     * @param line1 The second line.
18915     */
18916    dwv.math.getAngle = function(line0, line1) {
18917        var dx0 = line0.getDeltaX();
18918        var dy0 = line0.getDeltaY();
18919        var dx1 = line1.getDeltaX();
18920        var dy1 = line1.getDeltaY();
18921        // dot = ||a||*||b||*cos(theta)
18922        var dot = dx0 * dx1 + dy0 * dy1;
18923        // cross = ||a||*||b||*sin(theta)
18924        var det = dx0 * dy1 - dy0 * dx1;
18925        // tan = sin / cos
18926        var angle = Math.atan2(det, dot) * 180 / Math.PI;
18927        // complementary angle
18928        // shift?
18929        return 360 - (180 - angle);
18930    };
18931
18932    /**
18933     * Get a perpendicular line to an input one.
18934     * @param {Object} line The line to be perpendicular to.
18935     * @param {Object} point The middle point of the perpendicular line.
18936     * @param {Number} length The length of the perpendicular line.
18937     */
18938    dwv.math.getPerpendicularLine = function(line, point, length) {
18939        // begin point
18940        var beginX = 0;
18941        var beginY = 0;
18942        // end point
18943        var endX = 0;
18944        var endY = 0;
18945
18946        // check slope:
18947        // 0 -> horizontal
18948        // Infinite -> vertical (a/Infinite = 0)
18949        if (line.getSlope() !== 0) {
18950            // a0 * a1 = -1
18951            var slope = -1 / line.getSlope();
18952            // y0 = a1*x0 + b1 -> b1 = y0 - a1*x0
18953            var intercept = point.getY() - slope * point.getX();
18954
18955            // 1. (x - x0)^2 + (y - y0)^2 = d^2
18956            // 2. a = (y - y0) / (x - x0) -> y = a*(x - x0) + y0
18957            // ->  (x - x0)^2 + m^2 * (x - x0)^2 = d^2
18958            // -> x = x0 +- d / sqrt(1+m^2)
18959
18960            // length is the distance between begin and end,
18961            // point is half way between both -> d = length / 2
18962            var dx = length / (2 * Math.sqrt(1 + slope * slope));
18963
18964            // begin point
18965            beginX = point.getX() - dx;
18966            beginY = slope * beginX + intercept;
18967            // end point
18968            endX = point.getX() + dx;
18969            endY = slope * endX + intercept;
18970        } else {
18971            // horizontal input line -> perpendicular is vertical!
18972            // begin point
18973            beginX = point.getX();
18974            beginY = point.getY() - length / 2;
18975            // end point
18976            endX = point.getX();
18977            endY = point.getY() + length / 2;
18978        }
18979        // perpendicalar line
18980        return new dwv.math.Line(
18981            new dwv.math.Point2D(beginX, beginY),
18982            new dwv.math.Point2D(endX, endY));
18983    };
18984
18985    /**
18986     * Rectangle shape.
18987     * @constructor
18988     * @param {Object} begin A Point2D representing the beginning of the rectangle.
18989     * @param {Object} end A Point2D representing the end of the rectangle.
18990     */
18991    dwv.math.Rectangle = function(begin, end) {
18992        if (end.getX() < begin.getX()) {
18993            var tmpX = begin.getX();
18994            begin = new dwv.math.Point2D(end.getX(), begin.getY());
18995            end = new dwv.math.Point2D(tmpX, end.getY());
18996        }
18997        if (end.getY() < begin.getY()) {
18998            var tmpY = begin.getY();
18999            begin = new dwv.math.Point2D(begin.getX(), end.getY());
19000            end = new dwv.math.Point2D(end.getX(), tmpY);
19001        }
19002
19003        /**
19004         * Rectangle surface.
19005         * @private
19006         * @type Number
19007         */
19008        var surface = Math.abs(end.getX() - begin.getX()) * Math.abs(end.getY() - begin.getY());
19009
19010        /**
19011         * Get the begin point of the rectangle.
19012         * @return {Object} The begin point of the rectangle
19013         */
19014        this.getBegin = function() { return begin; };
19015        /**
19016         * Get the end point of the rectangle.
19017         * @return {Object} The end point of the rectangle
19018         */
19019        this.getEnd = function() { return end; };
19020        /**
19021         * Get the real width of the rectangle.
19022         * @return {Number} The real width of the rectangle.
19023         */
19024        this.getRealWidth = function() { return end.getX() - begin.getX(); };
19025        /**
19026         * Get the real height of the rectangle.
19027         * @return {Number} The real height of the rectangle.
19028         */
19029        this.getRealHeight = function() { return end.getY() - begin.getY(); };
19030        /**
19031         * Get the width of the rectangle.
19032         * @return {Number} The width of the rectangle.
19033         */
19034        this.getWidth = function() { return Math.abs(this.getRealWidth()); };
19035        /**
19036         * Get the height of the rectangle.
19037         * @return {Number} The height of the rectangle.
19038         */
19039        this.getHeight = function() { return Math.abs(this.getRealHeight()); };
19040        /**
19041         * Get the surface of the rectangle.
19042         * @return {Number} The surface of the rectangle.
19043         */
19044        this.getSurface = function() { return surface; };
19045        /**
19046         * Get the surface of the circle according to a spacing.
19047         * @param {Number} spacingX The X spacing.
19048         * @param {Number} spacingY The Y spacing.
19049         * @return {Number} The surface of the rectangle multiplied by the given
19050         *  spacing or null for null spacings.
19051         */
19052        this.getWorldSurface = function(spacingX, spacingY) {
19053            return mulABC(surface, spacingX, spacingY);
19054        };
19055    }; // Rectangle class
19056
19057    /**
19058     * Region Of Interest shape.
19059     * Note: should be a closed path.
19060     * @constructor
19061     */
19062    dwv.math.ROI = function() {
19063        /**
19064         * List of points.
19065         * @private
19066         * @type Array
19067         */
19068        var points = [];
19069
19070        /**
19071         * Get a point of the list at a given index.
19072         * @param {Number} index The index of the point to get (beware, no size check).
19073         * @return {Object} The Point2D at the given index.
19074         */
19075        this.getPoint = function(index) { return points[index]; };
19076        /**
19077         * Get the length of the point list.
19078         * @return {Number} The length of the point list.
19079         */
19080        this.getLength = function() { return points.length; };
19081        /**
19082         * Add a point to the ROI.
19083         * @param {Object} point The Point2D to add.
19084         */
19085        this.addPoint = function(point) { points.push(point); };
19086        /**
19087         * Add points to the ROI.
19088         * @param {Array} rhs The array of POints2D to add.
19089         */
19090        this.addPoints = function(rhs) { points = points.concat(rhs); };
19091    }; // ROI class
19092
19093    /**
19094     * Path shape.
19095     * @constructor
19096     * @param {Array} inputPointArray The list of Point2D that make the path (optional).
19097     * @param {Array} inputControlPointIndexArray The list of control point of path,
19098     *  as indexes (optional).
19099     * Note: first and last point do not need to be equal.
19100     */
19101    dwv.math.Path = function(inputPointArray, inputControlPointIndexArray) {
19102        /**
19103         * List of points.
19104         * @type Array
19105         */
19106        this.pointArray = inputPointArray ? inputPointArray.slice() : [];
19107        /**
19108         * List of control points.
19109         * @type Array
19110         */
19111        this.controlPointIndexArray = inputControlPointIndexArray ?
19112            inputControlPointIndexArray.slice() : [];
19113    }; // Path class
19114
19115    /**
19116     * Get a point of the list.
19117     * @param {Number} index The index of the point to get (beware, no size check).
19118     * @return {Object} The Point2D at the given index.
19119     */
19120    dwv.math.Path.prototype.getPoint = function(index) {
19121        return this.pointArray[index];
19122    };
19123
19124    /**
19125     * Is the given point a control point.
19126     * @param {Object} point The Point2D to check.
19127     * @return {Boolean} True if a control point.
19128     */
19129    dwv.math.Path.prototype.isControlPoint = function(point) {
19130        var index = this.pointArray.indexOf(point);
19131        if (index !== -1) {
19132            return this.controlPointIndexArray.indexOf(index) !== -1;
19133        } else {
19134            throw new Error("Error: isControlPoint called with not in list point.");
19135        }
19136    };
19137
19138    /**
19139     * Get the length of the path.
19140     * @return {Number} The length of the path.
19141     */
19142    dwv.math.Path.prototype.getLength = function() {
19143        return this.pointArray.length;
19144    };
19145
19146    /**
19147     * Add a point to the path.
19148     * @param {Object} point The Point2D to add.
19149     */
19150    dwv.math.Path.prototype.addPoint = function(point) {
19151        this.pointArray.push(point);
19152    };
19153
19154    /**
19155     * Add a control point to the path.
19156     * @param {Object} point The Point2D to make a control point.
19157     */
19158    dwv.math.Path.prototype.addControlPoint = function(point) {
19159        var index = this.pointArray.indexOf(point);
19160        if (index !== -1) {
19161            this.controlPointIndexArray.push(index);
19162        } else {
19163            throw new Error("Error: addControlPoint called with no point in list point.");
19164        }
19165    };
19166
19167    /**
19168     * Add points to the path.
19169     * @param {Array} points The list of Point2D to add.
19170     */
19171    dwv.math.Path.prototype.addPoints = function(newPointArray) {
19172        this.pointArray = this.pointArray.concat(newPointArray);
19173    };
19174
19175    /**
19176     * Append a Path to this one.
19177     * @param {Path} other The Path to append.
19178     */
19179    dwv.math.Path.prototype.appenPath = function(other) {
19180        var oldSize = this.pointArray.length;
19181        this.pointArray = this.pointArray.concat(other.pointArray);
19182        var indexArray = [];
19183        for (var i = 0; i < other.controlPointIndexArray.length; ++i) {
19184            indexArray[i] = other.controlPointIndexArray[i] + oldSize;
19185        }
19186        this.controlPointIndexArray = this.controlPointIndexArray.concat(indexArray);
19187    };
19188
19189    // namespaces
19190    var dwv = dwv || {};
19191    dwv.math = dwv.math || {};
19192
19193    /**
19194     * Get the minimum, maximum, mean and standard deviation
19195     * of an array of values.
19196     * Note: could use {@link https://github.com/tmcw/simple-statistics}.
19197     */
19198    dwv.math.getStats = function(array) {
19199        var min = array[0];
19200        var max = min;
19201        var mean = 0;
19202        var sum = 0;
19203        var sumSqr = 0;
19204        var stdDev = 0;
19205        var variance = 0;
19206
19207        var val = 0;
19208        for (var i = 0; i < array.length; ++i) {
19209            val = array[i];
19210            if (val < min) {
19211                min = val;
19212            } else if (val > max) {
19213                max = val;
19214            }
19215            sum += val;
19216            sumSqr += val * val;
19217        }
19218
19219        mean = sum / array.length;
19220        // see http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
19221        variance = sumSqr / array.length - mean * mean;
19222        stdDev = Math.sqrt(variance);
19223
19224        return { 'min': min, 'max': max, 'mean': mean, 'stdDev': stdDev };
19225    };
19226
19227    /**
19228     * Unique ID generator.
19229     * See {@link http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript}
19230     * and this {@link http://stackoverflow.com/a/13403498 answer}.
19231     */
19232    dwv.math.guid = function() {
19233        return Math.random().toString(36).substring(2, 15);
19234    };
19235
19236    // namespaces
19237    var dwv = dwv || {};
19238    dwv.math = dwv.math || {};
19239
19240    /**
19241     * Immutable 3D vector.
19242     * @constructor
19243     * @param {Number} x The X component of the vector.
19244     * @param {Number} y The Y component of the vector.
19245     * @param {Number} z The Z component of the vector.
19246     */
19247    dwv.math.Vector3D = function(x, y, z) {
19248        /**
19249         * Get the X component of the vector.
19250         * @return {Number} The X component of the vector.
19251         */
19252        this.getX = function() { return x; };
19253        /**
19254         * Get the Y component of the vector.
19255         * @return {Number} The Y component of the vector.
19256         */
19257        this.getY = function() { return y; };
19258        /**
19259         * Get the Z component of the vector.
19260         * @return {Number} The Z component of the vector.
19261         */
19262        this.getZ = function() { return z; };
19263    }; // Vector3D class
19264
19265    /**
19266     * Check for Vector3D equality.
19267     * @param {Object} rhs The other vector to compare to.
19268     * @return {Boolean} True if both vectors are equal.
19269     */
19270    dwv.math.Vector3D.prototype.equals = function(rhs) {
19271        return rhs !== null &&
19272            this.getX() === rhs.getX() &&
19273            this.getY() === rhs.getY() &&
19274            this.getZ() === rhs.getZ();
19275    };
19276
19277    /**
19278     * Get a string representation of the Vector3D.
19279     * @return {String} The vector as a string.
19280     */
19281    dwv.math.Vector3D.prototype.toString = function() {
19282        return "(" + this.getX() +
19283            ", " + this.getY() +
19284            ", " + this.getZ() + ")";
19285    };
19286
19287    /**
19288     * Get the norm of the vector.
19289     * @return {Number} The norm.
19290     */
19291    dwv.math.Vector3D.prototype.norm = function() {
19292        return Math.sqrt((this.getX() * this.getX()) +
19293            (this.getY() * this.getY()) +
19294            (this.getZ() * this.getZ()));
19295    };
19296
19297    /**
19298     * Get the cross product with another Vector3D, ie the
19299     * vector that is perpendicular to both a and b.
19300     * If both vectors are parallel, the cross product is a zero vector.
19301     * @param {Object} vector3D The input vector.
19302     * @return {Object} The result vector.
19303     */
19304    dwv.math.Vector3D.prototype.crossProduct = function(vector3D) {
19305        return new dwv.math.Vector3D(
19306            (this.getY() * vector3D.getZ()) - (vector3D.getY() * this.getZ()),
19307            (this.getZ() * vector3D.getX()) - (vector3D.getZ() * this.getX()),
19308            (this.getX() * vector3D.getY()) - (vector3D.getX() * this.getY()));
19309    };
19310
19311    /**
19312     * Get the dot product with another Vector3D.
19313     * @param {Object} vector3D The input vector.
19314     * @return {Number} The dot product.
19315     */
19316    dwv.math.Vector3D.prototype.dotProduct = function(vector3D) {
19317        return (this.getX() * vector3D.getX()) +
19318            (this.getY() * vector3D.getY()) +
19319            (this.getZ() * vector3D.getZ());
19320    };
19321
19322    // namespaces
19323    var dwv = dwv || {};
19324    dwv.tool = dwv.tool || {};
19325    // external
19326    var Konva = Konva || {};
19327
19328    /**
19329     * Arrow factory.
19330     * @constructor
19331     * @external Konva
19332     */
19333    dwv.tool.ArrowFactory = function() {
19334        /**
19335         * Get the number of points needed to build the shape.
19336         * @return {Number} The number of points.
19337         */
19338        this.getNPoints = function() { return 2; };
19339        /**
19340         * Get the timeout between point storage.
19341         * @return {Number} The timeout in milliseconds.
19342         */
19343        this.getTimeout = function() { return 0; };
19344    };
19345
19346    /**
19347     * Create an arrow shape to be displayed.
19348     * @param {Array} points The points from which to extract the line.
19349     * @param {Object} style The drawing style.
19350     * @param {Object} image The associated image.
19351     */
19352    dwv.tool.ArrowFactory.prototype.create = function(points, style /*, image*/ ) {
19353        // physical shape
19354        var line = new dwv.math.Line(points[0], points[1]);
19355        // draw shape
19356        var kshape = new Konva.Line({
19357            points: [line.getBegin().getX(), line.getBegin().getY(),
19358                line.getEnd().getX(), line.getEnd().getY()
19359            ],
19360            stroke: style.getLineColour(),
19361            strokeWidth: style.getScaledStrokeWidth(),
19362            name: "shape"
19363        });
19364        // larger hitfunc
19365        var linePerp0 = dwv.math.getPerpendicularLine(line, points[0], 10);
19366        var linePerp1 = dwv.math.getPerpendicularLine(line, points[1], 10);
19367        kshape.hitFunc(function(context) {
19368            context.beginPath();
19369            context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
19370            context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
19371            context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
19372            context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
19373            context.closePath();
19374            context.fillStrokeShape(this);
19375        });
19376        // triangle
19377        var beginTy = new dwv.math.Point2D(line.getBegin().getX(), line.getBegin().getY() - 10);
19378        var verticalLine = new dwv.math.Line(line.getBegin(), beginTy);
19379        var angle = dwv.math.getAngle(line, verticalLine);
19380        var angleRad = angle * Math.PI / 180;
19381        var radius = 5;
19382        var kpoly = new Konva.RegularPolygon({
19383            x: line.getBegin().getX() + radius * Math.sin(angleRad),
19384            y: line.getBegin().getY() + radius * Math.cos(angleRad),
19385            sides: 3,
19386            radius: radius,
19387            rotation: -angle,
19388            fill: style.getLineColour(),
19389            strokeWidth: style.getScaledStrokeWidth(),
19390            name: "shape-triangle"
19391        });
19392        // quantification
19393        var ktext = new Konva.Text({
19394            fontSize: style.getScaledFontSize(),
19395            fontFamily: style.getFontFamily(),
19396            fill: style.getLineColour(),
19397            name: "text"
19398        });
19399        ktext.textExpr = "";
19400        ktext.longText = "";
19401        ktext.quant = null;
19402        ktext.setText(ktext.textExpr);
19403        // label
19404        var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
19405        var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5;
19406        var klabel = new Konva.Label({
19407            x: line.getEnd().getX() + dX * 25,
19408            y: line.getEnd().getY() + dY * 15,
19409            name: "label"
19410        });
19411        klabel.add(ktext);
19412        klabel.add(new Konva.Tag());
19413
19414        // return group
19415        var group = new Konva.Group();
19416        group.name("line-group");
19417        group.add(kshape);
19418        group.add(kpoly);
19419        group.add(klabel);
19420        group.visible(true); // dont inherit
19421        return group;
19422    };
19423
19424    /**
19425     * Update an arrow shape.
19426     * @param {Object} anchor The active anchor.
19427     * @param {Object} image The associated image.
19428     */
19429    dwv.tool.UpdateArrow = function(anchor /*, image*/ ) {
19430        // parent group
19431        var group = anchor.getParent();
19432        // associated shape
19433        var kline = group.getChildren(function(node) {
19434            return node.name() === 'shape';
19435        })[0];
19436        // associated triangle shape
19437        var ktriangle = group.getChildren(function(node) {
19438            return node.name() === 'shape-triangle';
19439        })[0];
19440        // associated label
19441        var klabel = group.getChildren(function(node) {
19442            return node.name() === 'label';
19443        })[0];
19444        // find special points
19445        var begin = group.getChildren(function(node) {
19446            return node.id() === 'begin';
19447        })[0];
19448        var end = group.getChildren(function(node) {
19449            return node.id() === 'end';
19450        })[0];
19451        // update special points
19452        switch (anchor.id()) {
19453            case 'begin':
19454                begin.x(anchor.x());
19455                begin.y(anchor.y());
19456                break;
19457            case 'end':
19458                end.x(anchor.x());
19459                end.y(anchor.y());
19460                break;
19461        }
19462        // update shape and compensate for possible drag
19463        // note: shape.position() and shape.size() won't work...
19464        var bx = begin.x() - kline.x();
19465        var by = begin.y() - kline.y();
19466        var ex = end.x() - kline.x();
19467        var ey = end.y() - kline.y();
19468        kline.points([bx, by, ex, ey]);
19469        // new line
19470        var p2d0 = new dwv.math.Point2D(begin.x(), begin.y());
19471        var p2d1 = new dwv.math.Point2D(end.x(), end.y());
19472        var line = new dwv.math.Line(p2d0, p2d1);
19473        // larger hitfunc
19474        var p2b = new dwv.math.Point2D(bx, by);
19475        var p2e = new dwv.math.Point2D(ex, ey);
19476        var linePerp0 = dwv.math.getPerpendicularLine(line, p2b, 10);
19477        var linePerp1 = dwv.math.getPerpendicularLine(line, p2e, 10);
19478        kline.hitFunc(function(context) {
19479            context.beginPath();
19480            context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
19481            context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
19482            context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
19483            context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
19484            context.closePath();
19485            context.fillStrokeShape(this);
19486        });
19487        // udate triangle
19488        var beginTy = new dwv.math.Point2D(line.getBegin().getX(), line.getBegin().getY() - 10);
19489        var verticalLine = new dwv.math.Line(line.getBegin(), beginTy);
19490        var angle = dwv.math.getAngle(line, verticalLine);
19491        var angleRad = angle * Math.PI / 180;
19492        ktriangle.x(line.getBegin().getX() + ktriangle.radius() * Math.sin(angleRad));
19493        ktriangle.y(line.getBegin().getY() + ktriangle.radius() * Math.cos(angleRad));
19494        ktriangle.rotation(-angle);
19495        // update text
19496        var ktext = klabel.getText();
19497        ktext.quant = null;
19498        ktext.setText(ktext.textExpr);
19499        // update position
19500        var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
19501        var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5;
19502        var textPos = {
19503            'x': line.getEnd().getX() + dX * 25,
19504            'y': line.getEnd().getY() + dY * 15
19505        };
19506        klabel.position(textPos);
19507    };
19508
19509    // namespaces
19510    var dwv = dwv || {};
19511    /** @namespace */
19512    dwv.tool = dwv.tool || {};
19513    // external
19514    var Konva = Konva || {};
19515
19516    /**
19517     * Drawing tool.
19518     * @constructor
19519     * @param {Object} app The associated application.
19520     * @external Konva
19521     */
19522    dwv.tool.Draw = function(app, shapeFactoryList) {
19523        /**
19524         * Closure to self: to be used by event handlers.
19525         * @private
19526         * @type WindowLevel
19527         */
19528        var self = this;
19529        /**
19530         * Draw GUI.
19531         * @type Object
19532         */
19533        var gui = null;
19534        /**
19535         * Interaction start flag.
19536         * @private
19537         * @type Boolean
19538         */
19539        var started = false;
19540
19541        /**
19542         * Shape factory list
19543         * @type Object
19544         */
19545        this.shapeFactoryList = shapeFactoryList;
19546        /**
19547         * Draw command.
19548         * @private
19549         * @type Object
19550         */
19551        var command = null;
19552        /**
19553         * Current shape group.
19554         * @private
19555         * @type Object
19556         */
19557        var shapeGroup = null;
19558
19559        /**
19560         * Shape name.
19561         * @type String
19562         */
19563        this.shapeName = 0;
19564
19565        /**
19566         * List of points.
19567         * @private
19568         * @type Array
19569         */
19570        var points = [];
19571
19572        /**
19573         * Last selected point.
19574         * @private
19575         * @type Object
19576         */
19577        var lastPoint = null;
19578
19579        /**
19580         * Shape editor.
19581         * @private
19582         * @type Object
19583         */
19584        var shapeEditor = new dwv.tool.ShapeEditor(app);
19585
19586        // associate the event listeners of the editor
19587        //  with those of the draw tool
19588        shapeEditor.setDrawEventCallback(fireEvent);
19589
19590        /**
19591         * Trash draw: a cross.
19592         * @private
19593         * @type Object
19594         */
19595        var trash = new Konva.Group();
19596
19597        // first line of the cross
19598        var trashLine1 = new Konva.Line({
19599            points: [-10, -10, 10, 10],
19600            stroke: 'red'
19601        });
19602        // second line of the cross
19603        var trashLine2 = new Konva.Line({
19604            points: [10, -10, -10, 10],
19605            stroke: 'red'
19606        });
19607        trash.add(trashLine1);
19608        trash.add(trashLine2);
19609
19610        /**
19611         * Drawing style.
19612         * @type Style
19613         */
19614        this.style = new dwv.html.Style();
19615
19616        /**
19617         * Event listeners.
19618         * @private
19619         */
19620        var listeners = {};
19621
19622        /**
19623         * The associated draw layer.
19624         * @private
19625         * @type Object
19626         */
19627        var drawLayer = null;
19628
19629        /**
19630         * Handle mouse down event.
19631         * @param {Object} event The mouse down event.
19632         */
19633        this.mousedown = function(event) {
19634            // determine if the click happened in an existing shape
19635            var stage = app.getDrawStage();
19636            var kshape = stage.getIntersection({
19637                x: event._xs,
19638                y: event._ys
19639            });
19640
19641            if (kshape) {
19642                var group = kshape.getParent();
19643                var selectedShape = group.find(".shape")[0];
19644                // reset editor if click on other shape
19645                // (and avoid anchors mouse down)
19646                if (selectedShape && selectedShape !== shapeEditor.getShape()) {
19647                    shapeEditor.disable();
19648                    shapeEditor.setShape(selectedShape);
19649                    shapeEditor.setImage(app.getImage());
19650                    shapeEditor.enable();
19651                }
19652            } else {
19653                // disable edition
19654                shapeEditor.disable();
19655                shapeEditor.setShape(null);
19656                shapeEditor.setImage(null);
19657                // start storing points
19658                started = true;
19659                // clear array
19660                points = [];
19661                // store point
19662                lastPoint = new dwv.math.Point2D(event._x, event._y);
19663                points.push(lastPoint);
19664            }
19665        };
19666
19667        /**
19668         * Handle mouse move event.
19669         * @param {Object} event The mouse move event.
19670         */
19671        this.mousemove = function(event) {
19672            if (!started) {
19673                return;
19674            }
19675            if (Math.abs(event._x - lastPoint.getX()) > 0 ||
19676                Math.abs(event._y - lastPoint.getY()) > 0) {
19677                // current point
19678                lastPoint = new dwv.math.Point2D(event._x, event._y);
19679                // clear last added point from the list (but not the first one)
19680                if (points.length != 1) {
19681                    points.pop();
19682                }
19683                // add current one to the list
19684                points.push(lastPoint);
19685                // allow for anchor points
19686                var factory = new self.shapeFactoryList[self.shapeName]();
19687                if (points.length < factory.getNPoints()) {
19688                    clearTimeout(this.timer);
19689                    this.timer = setTimeout(function() {
19690                        points.push(lastPoint);
19691                    }, factory.getTimeout());
19692                }
19693                // remove previous draw
19694                if (shapeGroup) {
19695                    shapeGroup.destroy();
19696                }
19697                // create shape group
19698                shapeGroup = factory.create(points, self.style, app.getImage());
19699                // do not listen during creation
19700                var shape = shapeGroup.getChildren(function(node) {
19701                    return node.name() === 'shape';
19702                })[0];
19703                shape.listening(false);
19704                drawLayer.hitGraphEnabled(false);
19705                // draw shape command
19706                command = new dwv.tool.DrawGroupCommand(shapeGroup, self.shapeName, drawLayer, true);
19707                // draw
19708                command.execute();
19709            }
19710        };
19711
19712        /**
19713         * Handle mouse up event.
19714         * @param {Object} event The mouse up event.
19715         */
19716        this.mouseup = function( /*event*/ ) {
19717            if (started && points.length > 1) {
19718                // reset shape group
19719                if (shapeGroup) {
19720                    shapeGroup.destroy();
19721                }
19722                // create final shape
19723                var factory = new self.shapeFactoryList[self.shapeName]();
19724                var group = factory.create(points, self.style, app.getImage());
19725                group.id(dwv.math.guid());
19726                // re-activate layer
19727                drawLayer.hitGraphEnabled(true);
19728                // draw shape command
19729                command = new dwv.tool.DrawGroupCommand(group, self.shapeName, drawLayer);
19730                command.onExecute = fireEvent;
19731                command.onUndo = fireEvent;
19732                // execute it
19733                command.execute();
19734                // save it in undo stack
19735                app.addToUndoStack(command);
19736
19737                // set shape on
19738                var shape = group.getChildren(function(node) {
19739                    return node.name() === 'shape';
19740                })[0];
19741                self.setShapeOn(shape);
19742            }
19743            // reset flag
19744            started = false;
19745        };
19746
19747        /**
19748         * Handle mouse out event.
19749         * @param {Object} event The mouse out event.
19750         */
19751        this.mouseout = function(event) {
19752            self.mouseup(event);
19753        };
19754
19755        /**
19756         * Handle touch start event.
19757         * @param {Object} event The touch start event.
19758         */
19759        this.touchstart = function(event) {
19760            self.mousedown(event);
19761        };
19762
19763        /**
19764         * Handle touch move event.
19765         * @param {Object} event The touch move event.
19766         */
19767        this.touchmove = function(event) {
19768            self.mousemove(event);
19769        };
19770
19771        /**
19772         * Handle touch end event.
19773         * @param {Object} event The touch end event.
19774         */
19775        this.touchend = function(event) {
19776            self.mouseup(event);
19777        };
19778
19779        /**
19780         * Handle key down event.
19781         * @param {Object} event The key down event.
19782         */
19783        this.keydown = function(event) {
19784            app.onKeydown(event);
19785        };
19786
19787        /**
19788         * Setup the tool GUI.
19789         */
19790        this.setup = function() {
19791            gui = new dwv.gui.Draw(app);
19792            gui.setup(this.shapeFactoryList);
19793        };
19794
19795        /**
19796         * Enable the tool.
19797         * @param {Boolean} flag The flag to enable or not.
19798         */
19799        this.display = function(flag) {
19800            if (gui) {
19801                gui.display(flag);
19802            }
19803            // reset shape display properties
19804            shapeEditor.disable();
19805            shapeEditor.setShape(null);
19806            shapeEditor.setImage(null);
19807            document.body.style.cursor = 'default';
19808            // make layer listen or not to events
19809            app.getDrawStage().listening(flag);
19810            // get the current draw layer
19811            drawLayer = app.getCurrentDrawLayer();
19812            renderDrawLayer(flag);
19813            // listen to app change to update the draw layer
19814            if (flag) {
19815                app.addEventListener("slice-change", updateDrawLayer);
19816                app.addEventListener("frame-change", updateDrawLayer);
19817            } else {
19818                app.removeEventListener("slice-change", updateDrawLayer);
19819                app.removeEventListener("frame-change", updateDrawLayer);
19820            }
19821        };
19822
19823        /**
19824         * Get the current app draw layer.
19825         */
19826        function updateDrawLayer() {
19827            // deactivate the old draw layer
19828            renderDrawLayer(false);
19829            // get the current draw layer
19830            drawLayer = app.getCurrentDrawLayer();
19831            // activate the new draw layer
19832            renderDrawLayer(true);
19833        }
19834
19835        /**
19836         * Render (or not) the draw layer.
19837         * @param {Boolean} visible Set the draw layer visible or not.
19838         */
19839        function renderDrawLayer(visible) {
19840            drawLayer.listening(visible);
19841            drawLayer.hitGraphEnabled(visible);
19842            // get the list of shapes
19843            var groups = drawLayer.getChildren();
19844            var shapes = [];
19845            var fshape = function(node) {
19846                return node.name() === 'shape';
19847            };
19848            for (var i = 0; i < groups.length; ++i) {
19849                // should only be one shape per group
19850                shapes.push(groups[i].getChildren(fshape)[0]);
19851            }
19852            // set shape display properties
19853            if (visible) {
19854                app.addToolCanvasListeners(app.getDrawStage().getContent());
19855                shapes.forEach(function(shape) { self.setShapeOn(shape); });
19856            } else {
19857                app.removeToolCanvasListeners(app.getDrawStage().getContent());
19858                shapes.forEach(function(shape) { setShapeOff(shape); });
19859            }
19860            // draw
19861            drawLayer.draw();
19862        }
19863
19864        /**
19865         * Set shape off properties.
19866         * @param {Object} shape The shape to set off.
19867         */
19868        function setShapeOff(shape) {
19869            // mouse styling
19870            shape.off('mouseover');
19871            shape.off('mouseout');
19872            // drag
19873            shape.draggable(false);
19874            shape.off('dragstart');
19875            shape.off('dragmove');
19876            shape.off('dragend');
19877            shape.off('dblclick');
19878        }
19879
19880        /**
19881         * Get the real position from an event.
19882         */
19883        function getRealPosition(index) {
19884            var stage = app.getDrawStage();
19885            return {
19886                'x': stage.offset().x + index.x / stage.scale().x,
19887                'y': stage.offset().y + index.y / stage.scale().y
19888            };
19889        }
19890
19891        /**
19892         * Set shape on properties.
19893         * @param {Object} shape The shape to set on.
19894         */
19895        this.setShapeOn = function(shape) {
19896            // mouse over styling
19897            shape.on('mouseover', function() {
19898                document.body.style.cursor = 'pointer';
19899            });
19900            // mouse out styling
19901            shape.on('mouseout', function() {
19902                document.body.style.cursor = 'default';
19903            });
19904
19905            // make it draggable
19906            shape.draggable(true);
19907            var dragStartPos = null;
19908            var dragLastPos = null;
19909
19910            // command name based on shape type
19911            var shapeDisplayName = dwv.tool.GetShapeDisplayName(shape);
19912
19913            // store original colour
19914            var colour = null;
19915
19916            // save start position
19917            dragStartPos = { 'x': shape.x(), 'y': shape.y() };
19918
19919            // drag start event handling
19920            shape.on('dragstart', function( /*event*/ ) {
19921                // colour
19922                colour = shape.stroke();
19923                // display trash
19924                var stage = app.getDrawStage();
19925                var scale = stage.scale();
19926                var invscale = { 'x': 1 / scale.x, 'y': 1 / scale.y };
19927                trash.x(stage.offset().x + (256 / scale.x));
19928                trash.y(stage.offset().y + (20 / scale.y));
19929                trash.scale(invscale);
19930                drawLayer.add(trash);
19931                // deactivate anchors to avoid events on null shape
19932                shapeEditor.setAnchorsActive(false);
19933                // draw
19934                drawLayer.draw();
19935            });
19936            // drag move event handling
19937            shape.on('dragmove', function(event) {
19938                var pos = { 'x': this.x(), 'y': this.y() };
19939                var translation;
19940                if (dragLastPos) {
19941                    translation = {
19942                        'x': pos.x - dragLastPos.x,
19943                        'y': pos.y - dragLastPos.y
19944                    };
19945                } else {
19946                    translation = {
19947                        'x': pos.x - dragStartPos.x,
19948                        'y': pos.y - dragStartPos.y
19949                    };
19950                }
19951                dragLastPos = pos;
19952                // highlight trash when on it
19953                var offset = dwv.html.getEventOffset(event.evt)[0];
19954                var eventPos = getRealPosition(offset);
19955                if (Math.abs(eventPos.x - trash.x()) < 10 &&
19956                    Math.abs(eventPos.y - trash.y()) < 10) {
19957                    trash.getChildren().each(function(tshape) { tshape.stroke('orange'); });
19958                    shape.stroke('red');
19959                } else {
19960                    trash.getChildren().each(function(tshape) { tshape.stroke('red'); });
19961                    shape.stroke(colour);
19962                }
19963                // update group but not 'this' shape
19964                var group = this.getParent();
19965                group.getChildren().each(function(ashape) {
19966                    if (ashape === shape) {
19967                        return;
19968                    }
19969                    ashape.x(ashape.x() + translation.x);
19970                    ashape.y(ashape.y() + translation.y);
19971                });
19972                // reset anchors
19973                shapeEditor.resetAnchors();
19974                // draw
19975                drawLayer.draw();
19976            });
19977            // drag end event handling
19978            shape.on('dragend', function(event) {
19979                var pos = { 'x': this.x(), 'y': this.y() };
19980                dragLastPos = null;
19981                // remove trash
19982                trash.remove();
19983                // delete case
19984                var offset = dwv.html.getEventOffset(event.evt)[0];
19985                var eventPos = getRealPosition(offset);
19986                if (Math.abs(eventPos.x - trash.x()) < 10 &&
19987                    Math.abs(eventPos.y - trash.y()) < 10) {
19988                    // compensate for the drag translation
19989                    var delTranslation = {
19990                        'x': eventPos.x - dragStartPos.x,
19991                        'y': eventPos.y - dragStartPos.y
19992                    };
19993                    var group = this.getParent();
19994                    group.getChildren().each(function(ashape) {
19995                        ashape.x(ashape.x() - delTranslation.x);
19996                        ashape.y(ashape.y() - delTranslation.y);
19997                    });
19998                    // disable editor
19999                    shapeEditor.disable();
20000                    shapeEditor.setShape(null);
20001                    shapeEditor.setImage(null);
20002                    // reset
20003                    shape.stroke(colour);
20004                    document.body.style.cursor = 'default';
20005                    // delete command
20006                    var delcmd = new dwv.tool.DeleteGroupCommand(this.getParent(),
20007                        shapeDisplayName, drawLayer);
20008                    delcmd.onExecute = fireEvent;
20009                    delcmd.onUndo = fireEvent;
20010                    delcmd.execute();
20011                    app.addToUndoStack(delcmd);
20012                } else {
20013                    // save drag move
20014                    var translation = {
20015                        'x': pos.x - dragStartPos.x,
20016                        'y': pos.y - dragStartPos.y
20017                    };
20018                    if (translation.x !== 0 || translation.y !== 0) {
20019                        var mvcmd = new dwv.tool.MoveGroupCommand(this.getParent(),
20020                            shapeDisplayName, translation, drawLayer);
20021                        mvcmd.onExecute = fireEvent;
20022                        mvcmd.onUndo = fireEvent;
20023                        app.addToUndoStack(mvcmd);
20024
20025                        // reset start position
20026                        dragStartPos = { 'x': this.x(), 'y': this.y() };
20027                        // the move is handled by Konva, trigger an event manually
20028                        fireEvent({ 'type': 'draw-move' });
20029                    }
20030                    // reset anchors
20031                    shapeEditor.setAnchorsActive(true);
20032                    shapeEditor.resetAnchors();
20033                }
20034                // draw
20035                drawLayer.draw();
20036            });
20037            // double click handling: update label
20038            shape.on('dblclick', function() {
20039
20040                // get the label object for this shape
20041                var group = this.getParent();
20042                var labels = group.find('Label');
20043                // should just be one
20044                if (labels.length !== 1) {
20045                    throw new Error("Could not find the shape label.");
20046                }
20047                var ktext = labels[0].getText();
20048
20049                // ask user for new label
20050                var labelText = dwv.gui.prompt("Shape label", ktext.textExpr);
20051
20052                // if press cancel do nothing
20053                if (labelText === null) {
20054                    return;
20055                } else if (labelText === ktext.textExpr) {
20056                    return;
20057                }
20058                // update text expression and set text
20059                ktext.textExpr = labelText;
20060                ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
20061
20062                // trigger event
20063                fireEvent({ 'type': 'draw-change' });
20064
20065                // draw
20066                drawLayer.draw();
20067            });
20068        };
20069
20070        /**
20071         * Initialise the tool.
20072         */
20073        this.init = function() {
20074            // set the default to the first in the list
20075            var shapeName = 0;
20076            for (var key in this.shapeFactoryList) {
20077                shapeName = key;
20078                break;
20079            }
20080            this.setShapeName(shapeName);
20081            // init gui
20082            if (gui) {
20083                // init with the app window scale
20084                this.style.setScale(app.getWindowScale());
20085                // same for colour
20086                this.setLineColour(this.style.getLineColour());
20087                // init html
20088                gui.initialise();
20089            }
20090            return true;
20091        };
20092
20093        /**
20094         * Add an event listener on the app.
20095         * @param {String} type The event type.
20096         * @param {Object} listener The method associated with the provided event type.
20097         */
20098        this.addEventListener = function(type, listener) {
20099            if (typeof listeners[type] === "undefined") {
20100                listeners[type] = [];
20101            }
20102            listeners[type].push(listener);
20103        };
20104
20105        /**
20106         * Remove an event listener from the app.
20107         * @param {String} type The event type.
20108         * @param {Object} listener The method associated with the provided event type.
20109         */
20110        this.removeEventListener = function(type, listener) {
20111            if (typeof listeners[type] === "undefined") {
20112                return;
20113            }
20114            for (var i = 0; i < listeners[type].length; ++i) {
20115                if (listeners[type][i] === listener) {
20116                    listeners[type].splice(i, 1);
20117                }
20118            }
20119        };
20120
20121        /**
20122         * Set the line colour of the drawing.
20123         * @param {String} colour The colour to set.
20124         */
20125        this.setLineColour = function(colour) {
20126            this.style.setLineColour(colour);
20127        };
20128
20129        // Private Methods -----------------------------------------------------------
20130
20131        /**
20132         * Fire an event: call all associated listeners.
20133         * @param {Object} event The event to fire.
20134         */
20135        function fireEvent(event) {
20136            if (typeof listeners[event.type] === "undefined") {
20137                return;
20138            }
20139            for (var i = 0; i < listeners[event.type].length; ++i) {
20140                listeners[event.type][i](event);
20141            }
20142        }
20143
20144    }; // Draw class
20145
20146    /**
20147     * Help for this tool.
20148     * @return {Object} The help content.
20149     */
20150    dwv.tool.Draw.prototype.getHelp = function() {
20151        return {
20152            "title": dwv.i18n("tool.Draw.name"),
20153            "brief": dwv.i18n("tool.Draw.brief"),
20154            "mouse": {
20155                "mouse_drag": dwv.i18n("tool.Draw.mouse_drag")
20156            },
20157            "touch": {
20158                "touch_drag": dwv.i18n("tool.Draw.touch_drag")
20159            }
20160        };
20161    };
20162
20163    /**
20164     * Set the shape name of the drawing.
20165     * @param {String} name The name of the shape.
20166     */
20167    dwv.tool.Draw.prototype.setShapeName = function(name) {
20168        // check if we have it
20169        if (!this.hasShape(name)) {
20170            throw new Error("Unknown shape: '" + name + "'");
20171        }
20172        this.shapeName = name;
20173    };
20174
20175    /**
20176     * Check if the shape is in the shape list.
20177     * @param {String} name The name of the shape.
20178     */
20179    dwv.tool.Draw.prototype.hasShape = function(name) {
20180        return this.shapeFactoryList[name];
20181    };
20182
20183    // namespaces
20184    var dwv = dwv || {};
20185    /** @namespace */
20186    dwv.tool = dwv.tool || {};
20187    // external
20188    var Konva = Konva || {};
20189
20190    /**
20191     * Get the display name of the input shape.
20192     * @param {Object} shape The Konva shape.
20193     * @return {String} The display name.
20194     * @external Konva
20195     */
20196    dwv.tool.GetShapeDisplayName = function(shape) {
20197        var displayName = "shape";
20198        if (shape instanceof Konva.Line) {
20199            if (shape.points().length === 4) {
20200                displayName = "line";
20201            } else if (shape.points().length === 6) {
20202                displayName = "protractor";
20203            } else {
20204                displayName = "roi";
20205            }
20206        } else if (shape instanceof Konva.Rect) {
20207            displayName = "rectangle";
20208        } else if (shape instanceof Konva.Ellipse) {
20209            displayName = "ellipse";
20210        }
20211        // return
20212        return displayName;
20213    };
20214
20215    /**
20216     * Draw group command.
20217     * @param {Object} group The group draw.
20218     * @param {String} name The shape display name.
20219     * @param {Object} layer The layer where to draw the group.
20220     * @param {Object} silent Whether to send a creation event or not.
20221     * @constructor
20222     */
20223    dwv.tool.DrawGroupCommand = function(group, name, layer, silent) {
20224        var isSilent = (typeof silent === "undefined") ? false : true;
20225
20226        /**
20227         * Get the command name.
20228         * @return {String} The command name.
20229         */
20230        this.getName = function() { return "Draw-" + name; };
20231        /**
20232         * Execute the command.
20233         */
20234        this.execute = function() {
20235            // add the group to the layer
20236            layer.add(group);
20237            // draw
20238            layer.draw();
20239            // callback
20240            if (!isSilent) {
20241                this.onExecute({ 'type': 'draw-create', 'id': group.id() });
20242            }
20243        };
20244        /**
20245         * Undo the command.
20246         */
20247        this.undo = function() {
20248            // remove the group from the parent layer
20249            group.remove();
20250            // draw
20251            layer.draw();
20252            // callback
20253            this.onUndo({ 'type': 'draw-delete', 'id': group.id() });
20254        };
20255    }; // DrawGroupCommand class
20256
20257    /**
20258     * Handle an execute event.
20259     * @param {Object} event The execute event with type and id.
20260     */
20261    dwv.tool.DrawGroupCommand.prototype.onExecute = function( /*event*/ ) {
20262        // default does nothing.
20263    };
20264    /**
20265     * Handle an undo event.
20266     * @param {Object} event The undo event with type and id.
20267     */
20268    dwv.tool.DrawGroupCommand.prototype.onUndo = function( /*event*/ ) {
20269        // default does nothing.
20270    };
20271
20272    /**
20273     * Move group command.
20274     * @param {Object} group The group draw.
20275     * @param {String} name The shape display name.
20276     * @param {Object} translation A 2D translation to move the group by.
20277     * @param {Object} layer The layer where to move the group.
20278     * @constructor
20279     */
20280    dwv.tool.MoveGroupCommand = function(group, name, translation, layer) {
20281        /**
20282         * Get the command name.
20283         * @return {String} The command name.
20284         */
20285        this.getName = function() { return "Move-" + name; };
20286
20287        /**
20288         * Execute the command.
20289         */
20290        this.execute = function() {
20291            // translate all children of group
20292            group.getChildren().each(function(shape) {
20293                shape.x(shape.x() + translation.x);
20294                shape.y(shape.y() + translation.y);
20295            });
20296            // draw
20297            layer.draw();
20298            // callback
20299            this.onExecute({ 'type': 'draw-move', 'id': group.id() });
20300        };
20301        /**
20302         * Undo the command.
20303         */
20304        this.undo = function() {
20305            // invert translate all children of group
20306            group.getChildren().each(function(shape) {
20307                shape.x(shape.x() - translation.x);
20308                shape.y(shape.y() - translation.y);
20309            });
20310            // draw
20311            layer.draw();
20312            // callback
20313            this.onUndo({ 'type': 'draw-move', 'id': group.id() });
20314        };
20315    }; // MoveGroupCommand class
20316
20317    /**
20318     * Handle an execute event.
20319     * @param {Object} event The execute event with type and id.
20320     */
20321    dwv.tool.MoveGroupCommand.prototype.onExecute = function( /*event*/ ) {
20322        // default does nothing.
20323    };
20324    /**
20325     * Handle an undo event.
20326     * @param {Object} event The undo event with type and id.
20327     */
20328    dwv.tool.MoveGroupCommand.prototype.onUndo = function( /*event*/ ) {
20329        // default does nothing.
20330    };
20331
20332    /**
20333     * Change group command.
20334     * @param {String} name The shape display name.
20335     * @param {Object} func The change function.
20336     * @param {Object} startAnchor The anchor that starts the change.
20337     * @param {Object} endAnchor The anchor that ends the change.
20338     * @param {Object} layer The layer where to change the group.
20339     * @param {Object} image The associated image.
20340     * @constructor
20341     */
20342    dwv.tool.ChangeGroupCommand = function(name, func, startAnchor, endAnchor, layer, image) {
20343        /**
20344         * Get the command name.
20345         * @return {String} The command name.
20346         */
20347        this.getName = function() { return "Change-" + name; };
20348
20349        /**
20350         * Execute the command.
20351         */
20352        this.execute = function() {
20353            // change shape
20354            func(endAnchor, image);
20355            // draw
20356            layer.draw();
20357            // callback
20358            this.onExecute({ 'type': 'draw-change' });
20359        };
20360        /**
20361         * Undo the command.
20362         */
20363        this.undo = function() {
20364            // invert change shape
20365            func(startAnchor, image);
20366            // draw
20367            layer.draw();
20368            // callback
20369            this.onUndo({ 'type': 'draw-change' });
20370        };
20371    }; // ChangeGroupCommand class
20372
20373    /**
20374     * Handle an execute event.
20375     * @param {Object} event The execute event with type and id.
20376     */
20377    dwv.tool.ChangeGroupCommand.prototype.onExecute = function( /*event*/ ) {
20378        // default does nothing.
20379    };
20380    /**
20381     * Handle an undo event.
20382     * @param {Object} event The undo event with type and id.
20383     */
20384    dwv.tool.ChangeGroupCommand.prototype.onUndo = function( /*event*/ ) {
20385        // default does nothing.
20386    };
20387
20388    /**
20389     * Delete group command.
20390     * @param {Object} group The group draw.
20391     * @param {String} name The shape display name.
20392     * @param {Object} layer The layer where to delete the group.
20393     * @constructor
20394     */
20395    dwv.tool.DeleteGroupCommand = function(group, name, layer) {
20396        /**
20397         * Get the command name.
20398         * @return {String} The command name.
20399         */
20400        this.getName = function() { return "Delete-" + name; };
20401        /**
20402         * Execute the command.
20403         */
20404        this.execute = function() {
20405            // remove the group from the parent layer
20406            group.remove();
20407            // draw
20408            layer.draw();
20409            // callback
20410            this.onExecute({ 'type': 'draw-delete', 'id': group.id() });
20411        };
20412        /**
20413         * Undo the command.
20414         */
20415        this.undo = function() {
20416            // add the group to the layer
20417            layer.add(group);
20418            // draw
20419            layer.draw();
20420            // callback
20421            this.onUndo({ 'type': 'draw-create', 'id': group.id() });
20422        };
20423    }; // DeleteGroupCommand class
20424
20425    /**
20426     * Handle an execute event.
20427     * @param {Object} event The execute event with type and id.
20428     */
20429    dwv.tool.DeleteGroupCommand.prototype.onExecute = function( /*event*/ ) {
20430        // default does nothing.
20431    };
20432    /**
20433     * Handle an undo event.
20434     * @param {Object} event The undo event with type and id.
20435     */
20436    dwv.tool.DeleteGroupCommand.prototype.onUndo = function( /*event*/ ) {
20437        // default does nothing.
20438    };
20439
20440    // namespaces
20441    var dwv = dwv || {};
20442    dwv.tool = dwv.tool || {};
20443    // external
20444    var Konva = Konva || {};
20445
20446    /**
20447     * Shape editor.
20448     * @constructor
20449     * @external Konva
20450     */
20451    dwv.tool.ShapeEditor = function(app) {
20452        /**
20453         * Edited shape.
20454         * @private
20455         * @type Object
20456         */
20457        var shape = null;
20458        /**
20459         * Edited image. Used for quantification update.
20460         * @private
20461         * @type Object
20462         */
20463        var image = null;
20464        /**
20465         * Active flag.
20466         * @private
20467         * @type Boolean
20468         */
20469        var isActive = false;
20470        /**
20471         * Update function used by anchors to update the shape.
20472         * @private
20473         * @type Function
20474         */
20475        var updateFunction = null;
20476        /**
20477         * Draw event callback.
20478         * @private
20479         * @type Function
20480         */
20481        var drawEventCallback = null;
20482
20483        /**
20484         * Set the shape to edit.
20485         * @param {Object} inshape The shape to edit.
20486         */
20487        this.setShape = function(inshape) {
20488            shape = inshape;
20489            // reset anchors
20490            if (shape) {
20491                removeAnchors();
20492                addAnchors();
20493            }
20494        };
20495
20496        /**
20497         * Set the associated image.
20498         * @param {Object} img The associated image.
20499         */
20500        this.setImage = function(img) {
20501            image = img;
20502        };
20503
20504        /**
20505         * Get the edited shape.
20506         * @return {Object} The edited shape.
20507         */
20508        this.getShape = function() {
20509            return shape;
20510        };
20511
20512        /**
20513         * Get the active flag.
20514         * @return {Boolean} The active flag.
20515         */
20516        this.isActive = function() {
20517            return isActive;
20518        };
20519
20520        /**
20521         * Set the draw event callback.
20522         * @param {Object} callback The callback.
20523         */
20524        this.setDrawEventCallback = function(callback) {
20525            drawEventCallback = callback;
20526        };
20527
20528        /**
20529         * Enable the editor. Redraws the layer.
20530         */
20531        this.enable = function() {
20532            isActive = true;
20533            if (shape) {
20534                setAnchorsVisible(true);
20535                if (shape.getLayer()) {
20536                    shape.getLayer().draw();
20537                }
20538            }
20539        };
20540
20541        /**
20542         * Disable the editor. Redraws the layer.
20543         */
20544        this.disable = function() {
20545            isActive = false;
20546            if (shape) {
20547                setAnchorsVisible(false);
20548                if (shape.getLayer()) {
20549                    shape.getLayer().draw();
20550                }
20551            }
20552        };
20553
20554        /**
20555         * Reset the anchors.
20556         */
20557        this.resetAnchors = function() {
20558            // remove previous controls
20559            removeAnchors();
20560            // add anchors
20561            addAnchors();
20562            // set them visible
20563            setAnchorsVisible(true);
20564        };
20565
20566        /**
20567         * Apply a function on all anchors.
20568         * @param {Object} func A f(shape) function.
20569         */
20570        function applyFuncToAnchors(func) {
20571            if (shape && shape.getParent()) {
20572                var anchors = shape.getParent().find('.anchor');
20573                anchors.each(func);
20574            }
20575        }
20576
20577        /**
20578         * Set anchors visibility.
20579         * @param {Boolean} flag The visible flag.
20580         */
20581        function setAnchorsVisible(flag) {
20582            applyFuncToAnchors(function(anchor) {
20583                anchor.visible(flag);
20584            });
20585        }
20586
20587        /**
20588         * Set anchors active.
20589         * @param {Boolean} flag The active (on/off) flag.
20590         */
20591        this.setAnchorsActive = function(flag) {
20592            var func = null;
20593            if (flag) {
20594                func = function(anchor) {
20595                    setAnchorOn(anchor);
20596                };
20597            } else {
20598                func = function(anchor) {
20599                    setAnchorOff(anchor);
20600                };
20601            }
20602            applyFuncToAnchors(func);
20603        };
20604
20605        /**
20606         * Remove anchors.
20607         */
20608        function removeAnchors() {
20609            applyFuncToAnchors(function(anchor) {
20610                anchor.remove();
20611            });
20612        }
20613
20614        /**
20615         * Add the shape anchors.
20616         */
20617        function addAnchors() {
20618            // exit if no shape or no layer
20619            if (!shape || !shape.getLayer()) {
20620                return;
20621            }
20622            // get shape group
20623            var group = shape.getParent();
20624            // add shape specific anchors to the shape group
20625            if (shape instanceof Konva.Line) {
20626                var points = shape.points();
20627                if (points.length === 4 || points.length === 6) {
20628                    // add shape offset
20629                    var p0x = points[0] + shape.x();
20630                    var p0y = points[1] + shape.y();
20631                    var p1x = points[2] + shape.x();
20632                    var p1y = points[3] + shape.y();
20633                    addAnchor(group, p0x, p0y, 'begin');
20634                    if (points.length === 4) {
20635                        var shapekids = group.getChildren(function(node) {
20636                            return node.name().startsWith("shape-");
20637                        });
20638                        if (shapekids.length === 2) {
20639                            updateFunction = dwv.tool.UpdateRuler;
20640                        } else {
20641                            updateFunction = dwv.tool.UpdateArrow;
20642                        }
20643                        addAnchor(group, p1x, p1y, 'end');
20644                    } else {
20645                        updateFunction = dwv.tool.UpdateProtractor;
20646                        addAnchor(group, p1x, p1y, 'mid');
20647                        var p2x = points[4] + shape.x();
20648                        var p2y = points[5] + shape.y();
20649                        addAnchor(group, p2x, p2y, 'end');
20650                    }
20651                } else {
20652                    updateFunction = dwv.tool.UpdateRoi;
20653                    var px = 0;
20654                    var py = 0;
20655                    for (var i = 0; i < points.length; i = i + 2) {
20656                        px = points[i] + shape.x();
20657                        py = points[i + 1] + shape.y();
20658                        addAnchor(group, px, py, i);
20659                    }
20660                }
20661            } else if (shape instanceof Konva.Rect) {
20662                updateFunction = dwv.tool.UpdateRect;
20663                var rectX = shape.x();
20664                var rectY = shape.y();
20665                var rectWidth = shape.width();
20666                var rectHeight = shape.height();
20667                addAnchor(group, rectX, rectY, 'topLeft');
20668                addAnchor(group, rectX + rectWidth, rectY, 'topRight');
20669                addAnchor(group, rectX + rectWidth, rectY + rectHeight, 'bottomRight');
20670                addAnchor(group, rectX, rectY + rectHeight, 'bottomLeft');
20671            } else if (shape instanceof Konva.Ellipse) {
20672                updateFunction = dwv.tool.UpdateEllipse;
20673                var ellipseX = shape.x();
20674                var ellipseY = shape.y();
20675                var radius = shape.radius();
20676                addAnchor(group, ellipseX - radius.x, ellipseY - radius.y, 'topLeft');
20677                addAnchor(group, ellipseX + radius.x, ellipseY - radius.y, 'topRight');
20678                addAnchor(group, ellipseX + radius.x, ellipseY + radius.y, 'bottomRight');
20679                addAnchor(group, ellipseX - radius.x, ellipseY + radius.y, 'bottomLeft');
20680            }
20681            // add group to layer
20682            shape.getLayer().add(group);
20683        }
20684
20685        /**
20686         * Create shape editor controls, i.e. the anchors.
20687         * @param {Object} group The group associated with this anchor.
20688         * @param {Number} x The X position of the anchor.
20689         * @param {Number} y The Y position of the anchor.
20690         * @param {Number} id The id of the anchor.
20691         */
20692        function addAnchor(group, x, y, id) {
20693            // anchor shape
20694            var anchor = new Konva.Circle({
20695                x: x,
20696                y: y,
20697                stroke: '#999',
20698                fill: 'rgba(100,100,100,0.7',
20699                strokeWidth: app.getStyle().getScaledStrokeWidth() / app.getScale(),
20700                radius: app.getStyle().scale(6) / app.getScale(),
20701                name: 'anchor',
20702                id: id,
20703                dragOnTop: false,
20704                draggable: true,
20705                visible: false
20706            });
20707            // set anchor on
20708            setAnchorOn(anchor);
20709            // add the anchor to the group
20710            group.add(anchor);
20711        }
20712
20713        /**
20714         * Get a simple clone of the input anchor.
20715         * @param {Object} anchor The anchor to clone.
20716         */
20717        function getClone(anchor) {
20718            // create closure to properties
20719            var parent = anchor.getParent();
20720            var id = anchor.id();
20721            var x = anchor.x();
20722            var y = anchor.y();
20723            // create clone object
20724            var clone = {};
20725            clone.getParent = function() {
20726                return parent;
20727            };
20728            clone.id = function() {
20729                return id;
20730            };
20731            clone.x = function() {
20732                return x;
20733            };
20734            clone.y = function() {
20735                return y;
20736            };
20737            return clone;
20738        }
20739
20740        /**
20741         * Set the anchor on listeners.
20742         * @param {Object} anchor The anchor to set on.
20743         */
20744        function setAnchorOn(anchor) {
20745            var startAnchor = null;
20746
20747            // command name based on shape type
20748            var shapeDisplayName = dwv.tool.GetShapeDisplayName(shape);
20749
20750            // drag start listener
20751            anchor.on('dragstart', function() {
20752                startAnchor = getClone(this);
20753            });
20754            // drag move listener
20755            anchor.on('dragmove', function() {
20756                if (updateFunction) {
20757                    updateFunction(this, image);
20758                }
20759                if (this.getLayer()) {
20760                    this.getLayer().draw();
20761                } else {
20762                    console.warn("No layer to draw the anchor!");
20763                }
20764            });
20765            // drag end listener
20766            anchor.on('dragend', function() {
20767                var endAnchor = getClone(this);
20768                // store the change command
20769                var chgcmd = new dwv.tool.ChangeGroupCommand(
20770                    shapeDisplayName, updateFunction, startAnchor, endAnchor, this.getLayer(), image);
20771                chgcmd.onExecute = drawEventCallback;
20772                chgcmd.onUndo = drawEventCallback;
20773                chgcmd.execute();
20774                app.addToUndoStack(chgcmd);
20775                // reset start anchor
20776                startAnchor = endAnchor;
20777            });
20778            // mouse down listener
20779            anchor.on('mousedown touchstart', function() {
20780                this.moveToTop();
20781            });
20782            // mouse over styling
20783            anchor.on('mouseover', function() {
20784                document.body.style.cursor = 'pointer';
20785                this.stroke('#ddd');
20786                if (this.getLayer()) {
20787                    this.getLayer().draw();
20788                } else {
20789                    console.warn("No layer to draw the anchor!");
20790                }
20791            });
20792            // mouse out styling
20793            anchor.on('mouseout', function() {
20794                document.body.style.cursor = 'default';
20795                this.stroke('#999');
20796                if (this.getLayer()) {
20797                    this.getLayer().draw();
20798                } else {
20799                    console.warn("No layer to draw the anchor!");
20800                }
20801            });
20802        }
20803
20804        /**
20805         * Set the anchor off listeners.
20806         * @param {Object} anchor The anchor to set off.
20807         */
20808        function setAnchorOff(anchor) {
20809            anchor.off('dragstart');
20810            anchor.off('dragmove');
20811            anchor.off('dragend');
20812            anchor.off('mousedown touchstart');
20813            anchor.off('mouseover');
20814            anchor.off('mouseout');
20815        }
20816    };
20817
20818    // namespaces
20819    var dwv = dwv || {};
20820    dwv.tool = dwv.tool || {};
20821    // external
20822    var Konva = Konva || {};
20823
20824    /**
20825     * Ellipse factory.
20826     * @constructor
20827     * @external Konva
20828     */
20829    dwv.tool.EllipseFactory = function() {
20830        /**
20831         * Get the number of points needed to build the shape.
20832         * @return {Number} The number of points.
20833         */
20834        this.getNPoints = function() { return 2; };
20835        /**
20836         * Get the timeout between point storage.
20837         * @return {Number} The timeout in milliseconds.
20838         */
20839        this.getTimeout = function() { return 0; };
20840    };
20841
20842    /**
20843     * Create an ellipse shape to be displayed.
20844     * @param {Array} points The points from which to extract the ellipse.
20845     * @param {Object} style The drawing style.
20846     * @param {Object} image The associated image.
20847     */
20848    dwv.tool.EllipseFactory.prototype.create = function(points, style, image) {
20849        // calculate radius
20850        var a = Math.abs(points[0].getX() - points[1].getX());
20851        var b = Math.abs(points[0].getY() - points[1].getY());
20852        // physical shape
20853        var ellipse = new dwv.math.Ellipse(points[0], a, b);
20854        // draw shape
20855        var kshape = new Konva.Ellipse({
20856            x: ellipse.getCenter().getX(),
20857            y: ellipse.getCenter().getY(),
20858            radius: { x: ellipse.getA(), y: ellipse.getB() },
20859            stroke: style.getLineColour(),
20860            strokeWidth: style.getScaledStrokeWidth(),
20861            name: "shape"
20862        });
20863        // quantification
20864        var quant = image.quantifyEllipse(ellipse);
20865        var ktext = new Konva.Text({
20866            fontSize: style.getScaledFontSize(),
20867            fontFamily: style.getFontFamily(),
20868            fill: style.getLineColour(),
20869            name: "text"
20870        });
20871        ktext.textExpr = "{surface}";
20872        ktext.longText = "";
20873        ktext.quant = quant;
20874        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
20875        // label
20876        var klabel = new Konva.Label({
20877            x: ellipse.getCenter().getX(),
20878            y: ellipse.getCenter().getY(),
20879            name: "label"
20880        });
20881        klabel.add(ktext);
20882        klabel.add(new Konva.Tag());
20883
20884        // return group
20885        var group = new Konva.Group();
20886        group.name("ellipse-group");
20887        group.add(kshape);
20888        group.add(klabel);
20889        group.visible(true); // dont inherit
20890        return group;
20891    };
20892
20893    /**
20894     * Update an ellipse shape.
20895     * @param {Object} anchor The active anchor.
20896     * @param {Object} image The associated image.
20897     */
20898    dwv.tool.UpdateEllipse = function(anchor, image) {
20899        // parent group
20900        var group = anchor.getParent();
20901        // associated shape
20902        var kellipse = group.getChildren(function(node) {
20903            return node.name() === 'shape';
20904        })[0];
20905        // associated label
20906        var klabel = group.getChildren(function(node) {
20907            return node.name() === 'label';
20908        })[0];
20909        // find special points
20910        var topLeft = group.getChildren(function(node) {
20911            return node.id() === 'topLeft';
20912        })[0];
20913        var topRight = group.getChildren(function(node) {
20914            return node.id() === 'topRight';
20915        })[0];
20916        var bottomRight = group.getChildren(function(node) {
20917            return node.id() === 'bottomRight';
20918        })[0];
20919        var bottomLeft = group.getChildren(function(node) {
20920            return node.id() === 'bottomLeft';
20921        })[0];
20922        // update 'self' (undo case) and special points
20923        switch (anchor.id()) {
20924            case 'topLeft':
20925                topLeft.x(anchor.x());
20926                topLeft.y(anchor.y());
20927                topRight.y(anchor.y());
20928                bottomLeft.x(anchor.x());
20929                break;
20930            case 'topRight':
20931                topRight.x(anchor.x());
20932                topRight.y(anchor.y());
20933                topLeft.y(anchor.y());
20934                bottomRight.x(anchor.x());
20935                break;
20936            case 'bottomRight':
20937                bottomRight.x(anchor.x());
20938                bottomRight.y(anchor.y());
20939                bottomLeft.y(anchor.y());
20940                topRight.x(anchor.x());
20941                break;
20942            case 'bottomLeft':
20943                bottomLeft.x(anchor.x());
20944                bottomLeft.y(anchor.y());
20945                bottomRight.y(anchor.y());
20946                topLeft.x(anchor.x());
20947                break;
20948            default:
20949                console.error('Unhandled anchor id: ' + anchor.id());
20950                break;
20951        }
20952        // update shape
20953        var radiusX = (topRight.x() - topLeft.x()) / 2;
20954        var radiusY = (bottomRight.y() - topRight.y()) / 2;
20955        var center = { 'x': topLeft.x() + radiusX, 'y': topRight.y() + radiusY };
20956        kellipse.position(center);
20957        var radiusAbs = { 'x': Math.abs(radiusX), 'y': Math.abs(radiusY) };
20958        if (radiusAbs) {
20959            kellipse.radius(radiusAbs);
20960        }
20961        // new ellipse
20962        var ellipse = new dwv.math.Ellipse(center, radiusAbs.x, radiusAbs.y);
20963        // update text
20964        var quant = image.quantifyEllipse(ellipse);
20965        var ktext = klabel.getText();
20966        ktext.quant = quant;
20967        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
20968        // update position
20969        var textPos = { 'x': center.x, 'y': center.y };
20970        klabel.position(textPos);
20971    };
20972
20973    // namespaces
20974    var dwv = dwv || {};
20975    dwv.tool = dwv.tool || {};
20976    /** @namespace */
20977    dwv.tool.filter = dwv.tool.filter || {};
20978
20979    /**
20980     * Filter tool.
20981     * @constructor
20982     * @param {Array} filterList The list of filter objects.
20983     * @param {Object} app The associated app.
20984     */
20985    dwv.tool.Filter = function(filterList, app) {
20986        /**
20987         * Filter GUI.
20988         * @type Object
20989         */
20990        var gui = null;
20991        /**
20992         * Filter list
20993         * @type Object
20994         */
20995        this.filterList = filterList;
20996        /**
20997         * Selected filter.
20998         * @type Object
20999         */
21000        this.selectedFilter = 0;
21001        /**
21002         * Default filter name.
21003         * @type String
21004         */
21005        this.defaultFilterName = 0;
21006        /**
21007         * Display Flag.
21008         * @type Boolean
21009         */
21010        this.displayed = false;
21011        /**
21012         * Listener handler.
21013         * @type Object
21014         */
21015        var listenerHandler = new dwv.utils.ListenerHandler();
21016
21017        /**
21018         * Setup the filter GUI. Called at app startup.
21019         */
21020        this.setup = function() {
21021            if (Object.keys(this.filterList).length !== 0) {
21022                gui = new dwv.gui.Filter(app);
21023                gui.setup(this.filterList);
21024                for (var key in this.filterList) {
21025                    this.filterList[key].setup();
21026                    this.filterList[key].addEventListener("filter-run", fireEvent);
21027                    this.filterList[key].addEventListener("filter-undo", fireEvent);
21028                }
21029            }
21030        };
21031
21032        /**
21033         * Display the tool.
21034         * @param {Boolean} bool Flag to enable or not.
21035         */
21036        this.display = function(bool) {
21037            if (gui) {
21038                gui.display(bool);
21039            }
21040            this.displayed = bool;
21041            // display the selected filter
21042            this.selectedFilter.display(bool);
21043        };
21044
21045        /**
21046         * Initialise the filter. Called once the image is loaded.
21047         */
21048        this.init = function() {
21049            // set the default to the first in the list
21050            for (var key in this.filterList) {
21051                this.defaultFilterName = key;
21052                break;
21053            }
21054            this.setSelectedFilter(this.defaultFilterName);
21055            // init all filters
21056            for (key in this.filterList) {
21057                this.filterList[key].init();
21058            }
21059            // init html
21060            if (gui) {
21061                gui.initialise();
21062            }
21063            return true;
21064        };
21065
21066        /**
21067         * Handle keydown event.
21068         * @param {Object} event The keydown event.
21069         */
21070        this.keydown = function(event) {
21071            app.onKeydown(event);
21072        };
21073
21074        /**
21075         * Add an event listener to this class.
21076         * @param {String} type The event type.
21077         * @param {Object} callback The method associated with the provided event type,
21078         *    will be called with the fired event.
21079         */
21080        this.addEventListener = function(type, callback) {
21081            listenerHandler.add(type, callback);
21082        };
21083        /**
21084         * Remove an event listener from this class.
21085         * @param {String} type The event type.
21086         * @param {Object} callback The method associated with the provided event type.
21087         */
21088        this.removeEventListener = function(type, callback) {
21089            listenerHandler.remove(type, callback);
21090        };
21091        /**
21092         * Fire an event: call all associated listeners with the input event object.
21093         * @param {Object} event The event to fire.
21094         * @private
21095         */
21096        function fireEvent(event) {
21097            listenerHandler.fireEvent(event);
21098        }
21099
21100    }; // class dwv.tool.Filter
21101
21102    /**
21103     * Help for this tool.
21104     * @return {Object} The help content.
21105     */
21106    dwv.tool.Filter.prototype.getHelp = function() {
21107        return {
21108            "title": dwv.i18n("tool.Filter.name"),
21109            "brief": dwv.i18n("tool.Filter.brief")
21110        };
21111    };
21112
21113    /**
21114     * Get the selected filter.
21115     * @return {Object} The selected filter.
21116     */
21117    dwv.tool.Filter.prototype.getSelectedFilter = function() {
21118        return this.selectedFilter;
21119    };
21120
21121    /**
21122     * Set the selected filter.
21123     * @return {String} The name of the filter to select.
21124     */
21125    dwv.tool.Filter.prototype.setSelectedFilter = function(name) {
21126        // check if we have it
21127        if (!this.hasFilter(name)) {
21128            throw new Error("Unknown filter: '" + name + "'");
21129        }
21130        // hide last selected
21131        if (this.displayed) {
21132            this.selectedFilter.display(false);
21133        }
21134        // enable new one
21135        this.selectedFilter = this.filterList[name];
21136        // display the selected filter
21137        if (this.displayed) {
21138            this.selectedFilter.display(true);
21139        }
21140    };
21141
21142    /**
21143     * Get the list of filters.
21144     * @return {Array} The list of filter objects.
21145     */
21146    dwv.tool.Filter.prototype.getFilterList = function() {
21147        return this.filterList;
21148    };
21149
21150    /**
21151     * Check if a filter is in the filter list.
21152     * @param {String} name The name to check.
21153     * @return {String} The filter list element for the given name.
21154     */
21155    dwv.tool.Filter.prototype.hasFilter = function(name) {
21156        return this.filterList[name];
21157    };
21158
21159    /**
21160     * Threshold filter tool.
21161     * @constructor
21162     * @param {Object} app The associated application.
21163     */
21164    dwv.tool.filter.Threshold = function(app) {
21165        /**
21166         * Associated filter.
21167         * @type Object
21168         */
21169        var filter = new dwv.image.filter.Threshold();
21170        /**
21171         * Filter GUI.
21172         * @type Object
21173         */
21174        var gui = new dwv.gui.Threshold(app);
21175        /**
21176         * Flag to know wether to reset the image or not.
21177         * @type Boolean
21178         */
21179        var resetImage = true;
21180        /**
21181         * Listener handler.
21182         * @type Object
21183         */
21184        var listenerHandler = new dwv.utils.ListenerHandler();
21185
21186        /**
21187         * Setup the filter GUI. Called at app startup.
21188         */
21189        this.setup = function() {
21190            gui.setup();
21191        };
21192
21193        /**
21194         * Display the filter.
21195         * @param {Boolean} bool Flag to display or not.
21196         */
21197        this.display = function(bool) {
21198            gui.display(bool);
21199            // reset the image when the tool is displayed
21200            if (bool) {
21201                resetImage = true;
21202            }
21203        };
21204
21205        /**
21206         * Initialise the filter. Called once the image is loaded.
21207         */
21208        this.init = function() {
21209            gui.initialise();
21210        };
21211
21212        /**
21213         * Run the filter.
21214         * @param {Mixed} args The filter arguments.
21215         */
21216        this.run = function(args) {
21217            filter.setMin(args.min);
21218            filter.setMax(args.max);
21219            // reset the image if asked
21220            if (resetImage) {
21221                filter.setOriginalImage(app.getImage());
21222                resetImage = false;
21223            }
21224            var command = new dwv.tool.RunFilterCommand(filter, app);
21225            command.onExecute = fireEvent;
21226            command.onUndo = fireEvent;
21227            command.execute();
21228            // save command in undo stack
21229            app.addToUndoStack(command);
21230        };
21231
21232        /**
21233         * Add an event listener to this class.
21234         * @param {String} type The event type.
21235         * @param {Object} callback The method associated with the provided event type,
21236         *    will be called with the fired event.
21237         */
21238        this.addEventListener = function(type, callback) {
21239            listenerHandler.add(type, callback);
21240        };
21241        /**
21242         * Remove an event listener from this class.
21243         * @param {String} type The event type.
21244         * @param {Object} callback The method associated with the provided event type.
21245         */
21246        this.removeEventListener = function(type, callback) {
21247            listenerHandler.remove(type, callback);
21248        };
21249        /**
21250         * Fire an event: call all associated listeners with the input event object.
21251         * @param {Object} event The event to fire.
21252         * @private
21253         */
21254        function fireEvent(event) {
21255            listenerHandler.fireEvent(event);
21256        }
21257
21258    }; // class dwv.tool.filter.Threshold
21259
21260
21261    /**
21262     * Sharpen filter tool.
21263     * @constructor
21264     * @param {Object} app The associated application.
21265     */
21266    dwv.tool.filter.Sharpen = function(app) {
21267        /**
21268         * Filter GUI.
21269         * @type Object
21270         */
21271        var gui = new dwv.gui.Sharpen(app);
21272        /**
21273         * Listener handler.
21274         * @type Object
21275         */
21276        var listenerHandler = new dwv.utils.ListenerHandler();
21277
21278        /**
21279         * Setup the filter GUI. Called at app startup.
21280         */
21281        this.setup = function() {
21282            gui.setup();
21283        };
21284
21285        /**
21286         * Display the filter.
21287         * @param {Boolean} bool Flag to enable or not.
21288         */
21289        this.display = function(bool) {
21290            gui.display(bool);
21291        };
21292
21293        /**
21294         * Initialise the filter. Called once the image is loaded.
21295         */
21296        this.init = function() {
21297            // nothing to do...
21298        };
21299
21300        /**
21301         * Run the filter.
21302         * @param {Mixed} args The filter arguments.
21303         */
21304        this.run = function( /*args*/ ) {
21305            var filter = new dwv.image.filter.Sharpen();
21306            filter.setOriginalImage(app.getImage());
21307            var command = new dwv.tool.RunFilterCommand(filter, app);
21308            command.onExecute = fireEvent;
21309            command.onUndo = fireEvent;
21310            command.execute();
21311            // save command in undo stack
21312            app.addToUndoStack(command);
21313        };
21314
21315        /**
21316         * Add an event listener to this class.
21317         * @param {String} type The event type.
21318         * @param {Object} callback The method associated with the provided event type,
21319         *    will be called with the fired event.
21320         */
21321        this.addEventListener = function(type, callback) {
21322            listenerHandler.add(type, callback);
21323        };
21324        /**
21325         * Remove an event listener from this class.
21326         * @param {String} type The event type.
21327         * @param {Object} callback The method associated with the provided event type.
21328         */
21329        this.removeEventListener = function(type, callback) {
21330            listenerHandler.remove(type, callback);
21331        };
21332        /**
21333         * Fire an event: call all associated listeners with the input event object.
21334         * @param {Object} event The event to fire.
21335         * @private
21336         */
21337        function fireEvent(event) {
21338            listenerHandler.fireEvent(event);
21339        }
21340
21341    }; // dwv.tool.filter.Sharpen
21342
21343    /**
21344     * Sobel filter tool.
21345     * @constructor
21346     * @param {Object} app The associated application.
21347     */
21348    dwv.tool.filter.Sobel = function(app) {
21349        /**
21350         * Filter GUI.
21351         * @type Object
21352         */
21353        var gui = new dwv.gui.Sobel(app);
21354        /**
21355         * Listener handler.
21356         * @type Object
21357         */
21358        var listenerHandler = new dwv.utils.ListenerHandler();
21359
21360        /**
21361         * Setup the filter GUI. Called at app startup.
21362         */
21363        this.setup = function() {
21364            gui.setup();
21365        };
21366
21367        /**
21368         * Enable the filter.
21369         * @param {Boolean} bool Flag to enable or not.
21370         */
21371        this.display = function(bool) {
21372            gui.display(bool);
21373        };
21374
21375        /**
21376         * Initialise the filter. Called once the image is loaded.
21377         */
21378        this.init = function() {
21379            // nothing to do...
21380        };
21381
21382        /**
21383         * Run the filter.
21384         * @param {Mixed} args The filter arguments.
21385         */
21386        dwv.tool.filter.Sobel.prototype.run = function( /*args*/ ) {
21387            var filter = new dwv.image.filter.Sobel();
21388            filter.setOriginalImage(app.getImage());
21389            var command = new dwv.tool.RunFilterCommand(filter, app);
21390            command.onExecute = fireEvent;
21391            command.onUndo = fireEvent;
21392            command.execute();
21393            // save command in undo stack
21394            app.addToUndoStack(command);
21395        };
21396
21397        /**
21398         * Add an event listener to this class.
21399         * @param {String} type The event type.
21400         * @param {Object} callback The method associated with the provided event type,
21401         *    will be called with the fired event.
21402         */
21403        this.addEventListener = function(type, callback) {
21404            listenerHandler.add(type, callback);
21405        };
21406        /**
21407         * Remove an event listener from this class.
21408         * @param {String} type The event type.
21409         * @param {Object} callback The method associated with the provided event type.
21410         */
21411        this.removeEventListener = function(type, callback) {
21412            listenerHandler.remove(type, callback);
21413        };
21414        /**
21415         * Fire an event: call all associated listeners with the input event object.
21416         * @param {Object} event The event to fire.
21417         * @private
21418         */
21419        function fireEvent(event) {
21420            listenerHandler.fireEvent(event);
21421        }
21422
21423    }; // class dwv.tool.filter.Sobel
21424
21425    /**
21426     * Run filter command.
21427     * @constructor
21428     * @param {Object} filter The filter to run.
21429     * @param {Object} app The associated application.
21430     */
21431    dwv.tool.RunFilterCommand = function(filter, app) {
21432
21433        /**
21434         * Get the command name.
21435         * @return {String} The command name.
21436         */
21437        this.getName = function() { return "Filter-" + filter.getName(); };
21438
21439        /**
21440         * Execute the command.
21441         */
21442        this.execute = function() {
21443            // run filter and set app image
21444            app.setImage(filter.update());
21445            // update display
21446            app.render();
21447            // callback
21448            this.onExecute({ 'type': 'filter-run', 'id': this.getName() });
21449        };
21450
21451        /**
21452         * Undo the command.
21453         */
21454        this.undo = function() {
21455            // reset the image
21456            app.setImage(filter.getOriginalImage());
21457            // update display
21458            app.render();
21459            // callback
21460            this.onUndo({ 'type': 'filter-undo', 'id': this.getName() });
21461        };
21462
21463    }; // RunFilterCommand class
21464
21465    /**
21466     * Handle an execute event.
21467     * @param {Object} event The execute event with type and id.
21468     */
21469    dwv.tool.RunFilterCommand.prototype.onExecute = function( /*event*/ ) {
21470        // default does nothing.
21471    };
21472    /**
21473     * Handle an undo event.
21474     * @param {Object} event The undo event with type and id.
21475     */
21476    dwv.tool.RunFilterCommand.prototype.onUndo = function( /*event*/ ) {
21477        // default does nothing.
21478    };
21479
21480    // namespaces
21481    var dwv = dwv || {};
21482    dwv.tool = dwv.tool || {};
21483    // external
21484    var MagicWand = MagicWand || {};
21485
21486    /**
21487     * Floodfill painting tool.
21488     * @constructor
21489     * @param {Object} app The associated application.
21490     * @external MagicWand
21491     * @see {@link  https://github.com/Tamersoul/magic-wand-js}
21492     */
21493    dwv.tool.Floodfill = function(app) {
21494        /**
21495         * Original variables from external library. Used as in the lib example.
21496         * @private
21497         * @type Number
21498         */
21499        var blurRadius = 5;
21500        /**
21501         * Original variables from external library. Used as in the lib example.
21502         * @private
21503         * @type Number
21504         */
21505        var simplifyTolerant = 0;
21506        /**
21507         * Original variables from external library. Used as in the lib example.
21508         * @private
21509         * @type Number
21510         */
21511        var simplifyCount = 2000;
21512        /**
21513         * Canvas info
21514         * @private
21515         * @type Object
21516         */
21517        var imageInfo = null;
21518        /**
21519         * Object created by MagicWand lib containing border points
21520         * @private
21521         * @type Object
21522         */
21523        var mask = null;
21524        /**
21525         * threshold default tolerance of the tool border
21526         * @private
21527         * @type Number
21528         */
21529        var initialthreshold = 10;
21530        /**
21531         * threshold tolerance of the tool border
21532         * @private
21533         * @type Number
21534         */
21535        var currentthreshold = null;
21536        /**
21537         * Closure to self: to be used by event handlers.
21538         * @private
21539         * @type WindowLevel
21540         */
21541        var self = this;
21542        /**
21543         * Interaction start flag.
21544         * @type Boolean
21545         */
21546        this.started = false;
21547        /**
21548         * Livewire GUI.
21549         * @type Object
21550         */
21551        var gui = null;
21552        /**
21553         * Draw command.
21554         * @private
21555         * @type Object
21556         */
21557        var command = null;
21558        /**
21559         * Current shape group.
21560         * @private
21561         * @type Object
21562         */
21563        var shapeGroup = null;
21564        /**
21565         * Coordinates of the fist mousedown event.
21566         * @private
21567         * @type Object
21568         */
21569        var initialpoint;
21570        /**
21571         * Floodfill border.
21572         * @private
21573         * @type Object
21574         */
21575        var border = null;
21576        /**
21577         * List of parent points.
21578         * @private
21579         * @type Array
21580         */
21581        var parentPoints = [];
21582        /**
21583         * Assistant variable to paint border on all slices.
21584         * @private
21585         * @type Boolean
21586         */
21587        var extender = false;
21588        /**
21589         * Timeout for painting on mousemove.
21590         * @private
21591         */
21592        var painterTimeout;
21593        /**
21594         * Drawing style.
21595         * @type Style
21596         */
21597        this.style = new dwv.html.Style();
21598
21599        /**
21600         * Event listeners.
21601         * @private
21602         */
21603        var listeners = [];
21604
21605        /**
21606         * Set extend option for painting border on all slices.
21607         * @param {Boolean} The option to set
21608         */
21609        this.setExtend = function(Bool) {
21610            extender = Bool;
21611        };
21612
21613        /**
21614         * Get extend option for painting border on all slices.
21615         * @return {Boolean} The actual value of of the variable to use Floodfill on museup.
21616         */
21617        this.getExtend = function() {
21618            return extender;
21619        };
21620
21621        /**
21622         * Get (x, y) coordinates referenced to the canvas
21623         * @param {Object} event The original event.
21624         */
21625        var getCoord = function(event) {
21626            return { x: event._x, y: event._y };
21627        };
21628
21629        /**
21630         * Calculate border.
21631         * @private
21632         * @param {Object} Start point.
21633         * @param {Number} Threshold tolerance.
21634         */
21635        var calcBorder = function(points, threshold, simple) {
21636
21637            parentPoints = [];
21638            var image = {
21639                data: imageInfo.data,
21640                width: imageInfo.width,
21641                height: imageInfo.height,
21642                bytes: 4
21643            };
21644
21645            // var p = new dwv.math.FastPoint2D(points.x, points.y);
21646            mask = MagicWand.floodFill(image, points.x, points.y, threshold);
21647            mask = MagicWand.gaussBlurOnlyBorder(mask, blurRadius);
21648
21649            var cs = MagicWand.traceContours(mask);
21650            cs = MagicWand.simplifyContours(cs, simplifyTolerant, simplifyCount);
21651
21652            if (cs.length > 0 && cs[0].points[0].x) {
21653                if (simple) {
21654                    return cs[0].points;
21655                }
21656                for (var j = 0, icsl = cs[0].points.length; j < icsl; j++) {
21657                    parentPoints.push(new dwv.math.Point2D(cs[0].points[j].x, cs[0].points[j].y));
21658                }
21659                return parentPoints;
21660            } else {
21661                return false;
21662            }
21663        };
21664
21665        /**
21666         * Paint Floodfill.
21667         * @private
21668         * @param {Object} Start point.
21669         * @param {Number} Threshold tolerance.
21670         */
21671        var paintBorder = function(point, threshold) {
21672            // Calculate the border
21673            border = calcBorder(point, threshold);
21674            // Paint the border
21675            if (border) {
21676                var factory = new dwv.tool.RoiFactory();
21677                shapeGroup = factory.create(border, self.style);
21678                shapeGroup.id(dwv.math.guid());
21679                // draw shape command
21680                command = new dwv.tool.DrawGroupCommand(shapeGroup, "floodfill", app.getCurrentDrawLayer());
21681                command.onExecute = fireEvent;
21682                command.onUndo = fireEvent;
21683                // // draw
21684                command.execute();
21685                // save it in undo stack
21686                app.addToUndoStack(command);
21687
21688                return true;
21689            } else {
21690                return false;
21691            }
21692        };
21693
21694        /**
21695         * Create Floodfill in all the prev and next slices while border is found
21696         */
21697        this.extend = function(ini, end) {
21698            //avoid errors
21699            if (!initialpoint) {
21700                throw "'initialpoint' not found. User must click before use extend!";
21701            }
21702            // remove previous draw
21703            if (shapeGroup) {
21704                shapeGroup.destroy();
21705            }
21706
21707            var pos = app.getViewController().getCurrentPosition();
21708            var threshold = currentthreshold || initialthreshold;
21709
21710            // Iterate over the next images and paint border on each slice.
21711            for (var i = pos.k, len = end ? end : app.getImage().getGeometry().getSize().getNumberOfSlices(); i < len; i++) {
21712                if (!paintBorder(initialpoint, threshold)) {
21713                    break;
21714                }
21715                app.getViewController().incrementSliceNb();
21716            }
21717            app.getViewController().setCurrentPosition(pos);
21718
21719            // Iterate over the prev images and paint border on each slice.
21720            for (var j = pos.k, jl = ini ? ini : 0; j > jl; j--) {
21721                if (!paintBorder(initialpoint, threshold)) {
21722                    break;
21723                }
21724                app.getViewController().decrementSliceNb();
21725            }
21726            app.getViewController().setCurrentPosition(pos);
21727        };
21728
21729        /**
21730         * Modify tolerance threshold and redraw ROI.
21731         * @param {Number} New threshold.
21732         */
21733        this.modifyThreshold = function(modifyThreshold, shape) {
21734
21735            if (!shape && shapeGroup) {
21736                shape = shapeGroup.getChildren(function(node) {
21737                    return node.name() === 'shape';
21738                })[0];
21739            } else {
21740                throw 'No shape found';
21741            }
21742
21743            clearTimeout(painterTimeout);
21744            painterTimeout = setTimeout(function() {
21745                border = calcBorder(initialpoint, modifyThreshold, true);
21746                if (!border) {
21747                    return false;
21748                }
21749                var arr = [];
21750                for (var i = 0, bl = border.length; i < bl; ++i) {
21751                    arr.push(border[i].x);
21752                    arr.push(border[i].y);
21753                }
21754                shape.setPoints(arr);
21755                var shapeLayer = shape.getLayer();
21756                shapeLayer.draw();
21757                self.onThresholdChange(modifyThreshold);
21758            }, 100);
21759        };
21760
21761        /**
21762         * Event fired when threshold change
21763         * @param {Number} Current threshold
21764         */
21765        this.onThresholdChange = function( /*value*/ ) {
21766            // Defaults do nothing
21767        };
21768
21769        /**
21770         * Handle mouse down event.
21771         * @param {Object} event The mouse down event.
21772         */
21773        this.mousedown = function(event) {
21774            imageInfo = app.getImageData();
21775            if (!imageInfo) { return console.error('No image found'); }
21776
21777            self.started = true;
21778            initialpoint = getCoord(event);
21779            paintBorder(initialpoint, initialthreshold);
21780            self.onThresholdChange(initialthreshold);
21781        };
21782
21783        /**
21784         * Handle mouse move event.
21785         * @param {Object} event The mouse move event.
21786         */
21787        this.mousemove = function(event) {
21788            if (!self.started) {
21789                return;
21790            }
21791            var movedpoint = getCoord(event);
21792            currentthreshold = Math.round(Math.sqrt(Math.pow((initialpoint.x - movedpoint.x), 2) + Math.pow((initialpoint.y - movedpoint.y), 2)) / 2);
21793            currentthreshold = currentthreshold < initialthreshold ? initialthreshold : currentthreshold - initialthreshold;
21794            self.modifyThreshold(currentthreshold);
21795        };
21796
21797        /**
21798         * Handle mouse up event.
21799         * @param {Object} event The mouse up event.
21800         */
21801        this.mouseup = function( /*event*/ ) {
21802            self.started = false;
21803            if (extender) {
21804                self.extend();
21805            }
21806        };
21807
21808        /**
21809         * Handle mouse out event.
21810         * @param {Object} event The mouse out event.
21811         */
21812        this.mouseout = function( /*event*/ ) {
21813            self.mouseup( /*event*/ );
21814        };
21815
21816        /**
21817         * Handle touch start event.
21818         * @param {Object} event The touch start event.
21819         */
21820        this.touchstart = function(event) {
21821            // treat as mouse down
21822            self.mousedown(event);
21823        };
21824
21825        /**
21826         * Handle touch move event.
21827         * @param {Object} event The touch move event.
21828         */
21829        this.touchmove = function(event) {
21830            // treat as mouse move
21831            self.mousemove(event);
21832        };
21833
21834        /**
21835         * Handle touch end event.
21836         * @param {Object} event The touch end event.
21837         */
21838        this.touchend = function( /*event*/ ) {
21839            // treat as mouse up
21840            self.mouseup( /*event*/ );
21841        };
21842
21843        /**
21844         * Handle key down event.
21845         * @param {Object} event The key down event.
21846         */
21847        this.keydown = function(event) {
21848            app.onKeydown(event);
21849        };
21850
21851        /**
21852         * Setup the tool GUI.
21853         */
21854        this.setup = function() {
21855            gui = new dwv.gui.ColourTool(app, "ff");
21856            gui.setup();
21857        };
21858
21859        /**
21860         * Enable the tool.
21861         * @param {Boolean} bool The flag to enable or not.
21862         */
21863        this.display = function(bool) {
21864            if (gui) {
21865                gui.display(bool);
21866            }
21867            // TODO why twice?
21868            this.init();
21869        };
21870
21871        /**
21872         * Initialise the tool.
21873         */
21874        this.init = function() {
21875            if (gui) {
21876                // init with the app window scale
21877                this.style.setScale(app.getWindowScale());
21878                // set the default to the first in the list
21879                this.setLineColour(this.style.getLineColour());
21880                // init html
21881                gui.initialise();
21882            }
21883
21884            return true;
21885        };
21886
21887        /**
21888         * Add an event listener on the app.
21889         * @param {Object} listener The method associated with the provided event type.
21890         */
21891        this.addEventListener = function(listener) {
21892            listeners.push(listener);
21893        };
21894
21895        /**
21896         * Remove an event listener from the app.
21897         * @param {Object} listener The method associated with the provided event type.
21898         */
21899        this.removeEventListener = function(listener) {
21900            for (var i = 0; i < listeners.length; ++i) {
21901                if (listeners[i] === listener) {
21902                    listeners.splice(i, 1);
21903                }
21904            }
21905        };
21906
21907        // Private Methods -----------------------------------------------------------
21908
21909        /**
21910         * Fire an event: call all associated listeners.
21911         * @param {Object} event The event to fire.
21912         */
21913        function fireEvent(event) {
21914            for (var i = 0; i < listeners.length; ++i) {
21915                listeners[i](event);
21916            }
21917        }
21918
21919    }; // Floodfill class
21920
21921    /**
21922     * Help for this tool.
21923     * @return {Object} The help content.
21924     */
21925    dwv.tool.Floodfill.prototype.getHelp = function() {
21926        return {
21927            'title': dwv.i18n("tool.Floodfill.name"),
21928            'brief': dwv.i18n("tool.Floodfill.brief"),
21929            "mouse": {
21930                "click": dwv.i18n("tool.Floodfill.click")
21931            },
21932            "touch": {
21933                "tap": dwv.i18n("tool.Floodfill.tap")
21934            }
21935        };
21936    };
21937
21938    /**
21939     * Set the line colour of the drawing.
21940     * @param {String} colour The colour to set.
21941     */
21942    dwv.tool.Floodfill.prototype.setLineColour = function(colour) {
21943        // set style var
21944        this.style.setLineColour(colour);
21945    };
21946
21947    // namespaces
21948    var dwv = dwv || {};
21949    dwv.tool = dwv.tool || {};
21950    // external
21951    var Konva = Konva || {};
21952
21953    /**
21954     * FreeHand factory.
21955     * @constructor
21956     * @external Konva
21957     */
21958    dwv.tool.FreeHandFactory = function() {
21959        /**
21960         * Get the number of points needed to build the shape.
21961         * @return {Number} The number of points.
21962         */
21963        this.getNPoints = function() { return 1000; };
21964        /**
21965         * Get the timeout between point storage.
21966         * @return {Number} The timeout in milliseconds.
21967         */
21968        this.getTimeout = function() { return 25; };
21969    };
21970
21971    /**
21972     * Create a roi shape to be displayed.
21973     * @param {Array} points The points from which to extract the line.
21974     * @param {Object} style The drawing style.
21975     * @param {Object} image The associated image.
21976     */
21977    dwv.tool.FreeHandFactory.prototype.create = function(points, style /*, image*/ ) {
21978        // points stored the Konvajs way
21979        var arr = [];
21980        for (var i = 0; i < points.length; ++i) {
21981            arr.push(points[i].getX());
21982            arr.push(points[i].getY());
21983        }
21984        // draw shape
21985        var kshape = new Konva.Line({
21986            points: arr,
21987            stroke: style.getLineColour(),
21988            strokeWidth: style.getScaledStrokeWidth(),
21989            name: "shape",
21990            tension: 0.5
21991        });
21992
21993        // text
21994        var ktext = new Konva.Text({
21995            fontSize: style.getScaledFontSize(),
21996            fontFamily: style.getFontFamily(),
21997            fill: style.getLineColour(),
21998            name: "text"
21999        });
22000        ktext.textExpr = "";
22001        ktext.longText = "";
22002        ktext.quant = null;
22003        ktext.setText(ktext.textExpr);
22004
22005        // label
22006        var klabel = new Konva.Label({
22007            x: points[0].getX(),
22008            y: points[0].getY() + 10,
22009            name: "label"
22010        });
22011        klabel.add(ktext);
22012        klabel.add(new Konva.Tag());
22013
22014        // return group
22015        var group = new Konva.Group();
22016        group.name("freeHand-group");
22017        group.add(kshape);
22018        group.add(klabel);
22019        group.visible(true); // dont inherit
22020        return group;
22021    };
22022
22023    /**
22024     * Update a FreeHand shape.
22025     * @param {Object} anchor The active anchor.
22026     * @param {Object} image The associated image.
22027     */
22028    dwv.tool.UpdateFreeHand = function(anchor /*, image*/ ) {
22029        // parent group
22030        var group = anchor.getParent();
22031        // associated shape
22032        var kline = group.getChildren(function(node) {
22033            return node.name() === 'shape';
22034        })[0];
22035        // associated label
22036        var klabel = group.getChildren(function(node) {
22037            return node.name() === 'label';
22038        })[0];
22039
22040        // update self
22041        var point = group.getChildren(function(node) {
22042            return node.id() === anchor.id();
22043        })[0];
22044        point.x(anchor.x());
22045        point.y(anchor.y());
22046        // update the roi point and compensate for possible drag
22047        // (the anchor id is the index of the point in the list)
22048        var points = kline.points();
22049        points[anchor.id()] = anchor.x() - kline.x();
22050        points[anchor.id() + 1] = anchor.y() - kline.y();
22051        kline.points(points);
22052
22053        // update text
22054        var ktext = klabel.getText();
22055        ktext.quant = null;
22056        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22057        // update position
22058        var textPos = { 'x': points[0] + kline.x(), 'y': points[1] + kline.y() + 10 };
22059        klabel.position(textPos);
22060    };
22061
22062    // namespaces
22063    var dwv = dwv || {};
22064    dwv.tool = dwv.tool || {};
22065
22066    /**
22067     * Livewire painting tool.
22068     * @constructor
22069     * @param {Object} app The associated application.
22070     */
22071    dwv.tool.Livewire = function(app) {
22072        /**
22073         * Closure to self: to be used by event handlers.
22074         * @private
22075         * @type WindowLevel
22076         */
22077        var self = this;
22078        /**
22079         * Livewire GUI.
22080         * @type Object
22081         */
22082        var gui = null;
22083        /**
22084         * Interaction start flag.
22085         * @type Boolean
22086         */
22087        this.started = false;
22088
22089        /**
22090         * Draw command.
22091         * @private
22092         * @type Object
22093         */
22094        var command = null;
22095        /**
22096         * Current shape group.
22097         * @private
22098         * @type Object
22099         */
22100        var shapeGroup = null;
22101        /**
22102         * Drawing style.
22103         * @type Style
22104         */
22105        this.style = new dwv.html.Style();
22106        // init with the app window scale
22107        this.style.setScale(app.getWindowScale());
22108
22109        /**
22110         * Path storage. Paths are stored in reverse order.
22111         * @private
22112         * @type Path
22113         */
22114        var path = new dwv.math.Path();
22115        /**
22116         * Current path storage. Paths are stored in reverse order.
22117         * @private
22118         * @type Path
22119         */
22120        var currentPath = new dwv.math.Path();
22121        /**
22122         * List of parent points.
22123         * @private
22124         * @type Array
22125         */
22126        var parentPoints = [];
22127        /**
22128         * Tolerance.
22129         * @private
22130         * @type Number
22131         */
22132        var tolerance = 5;
22133
22134        /**
22135         * Event listeners.
22136         * @private
22137         */
22138        var listeners = [];
22139
22140        /**
22141         * Clear the parent points list.
22142         * @private
22143         */
22144        function clearParentPoints() {
22145            var nrows = app.getImage().getGeometry().getSize().getNumberOfRows();
22146            for (var i = 0; i < nrows; ++i) {
22147                parentPoints[i] = [];
22148            }
22149        }
22150
22151        /**
22152         * Clear the stored paths.
22153         * @private
22154         */
22155        function clearPaths() {
22156            path = new dwv.math.Path();
22157            currentPath = new dwv.math.Path();
22158        }
22159
22160        /**
22161         * Scissor representation.
22162         * @private
22163         * @type Scissors
22164         */
22165        var scissors = new dwv.math.Scissors();
22166
22167        /**
22168         * Handle mouse down event.
22169         * @param {Object} event The mouse down event.
22170         */
22171        this.mousedown = function(event) {
22172            // first time
22173            if (!self.started) {
22174                self.started = true;
22175                self.x0 = event._x;
22176                self.y0 = event._y;
22177                // clear vars
22178                clearPaths();
22179                clearParentPoints();
22180                // do the training from the first point
22181                var p = new dwv.math.FastPoint2D(event._x, event._y);
22182                scissors.doTraining(p);
22183                // add the initial point to the path
22184                var p0 = new dwv.math.Point2D(event._x, event._y);
22185                path.addPoint(p0);
22186                path.addControlPoint(p0);
22187            } else {
22188                // final point: at 'tolerance' of the initial point
22189                if ((Math.abs(event._x - self.x0) < tolerance) && (Math.abs(event._y - self.y0) < tolerance)) {
22190                    // draw
22191                    self.mousemove(event);
22192                    // listen
22193                    command.onExecute = fireEvent;
22194                    command.onUndo = fireEvent;
22195                    // debug
22196                    console.log("Done.");
22197                    // save command in undo stack
22198                    app.addToUndoStack(command);
22199                    // set flag
22200                    self.started = false;
22201                }
22202                // anchor point
22203                else {
22204                    path = currentPath;
22205                    clearParentPoints();
22206                    var pn = new dwv.math.FastPoint2D(event._x, event._y);
22207                    scissors.doTraining(pn);
22208                    path.addControlPoint(currentPath.getPoint(0));
22209                }
22210            }
22211        };
22212
22213        /**
22214         * Handle mouse move event.
22215         * @param {Object} event The mouse move event.
22216         */
22217        this.mousemove = function(event) {
22218            if (!self.started) {
22219                return;
22220            }
22221            // set the point to find the path to
22222            var p = new dwv.math.FastPoint2D(event._x, event._y);
22223            scissors.setPoint(p);
22224            // do the work
22225            var results = 0;
22226            var stop = false;
22227            while (!parentPoints[p.y][p.x] && !stop) {
22228                console.log("Getting ready...");
22229                results = scissors.doWork();
22230
22231                if (results.length === 0) {
22232                    stop = true;
22233                } else {
22234                    // fill parents
22235                    for (var i = 0; i < results.length - 1; i += 2) {
22236                        var _p = results[i];
22237                        var _q = results[i + 1];
22238                        parentPoints[_p.y][_p.x] = _q;
22239                    }
22240                }
22241            }
22242            console.log("Ready!");
22243
22244            // get the path
22245            currentPath = new dwv.math.Path();
22246            stop = false;
22247            while (p && !stop) {
22248                currentPath.addPoint(new dwv.math.Point2D(p.x, p.y));
22249                if (!parentPoints[p.y]) {
22250                    stop = true;
22251                } else {
22252                    if (!parentPoints[p.y][p.x]) {
22253                        stop = true;
22254                    } else {
22255                        p = parentPoints[p.y][p.x];
22256                    }
22257                }
22258            }
22259            currentPath.appenPath(path);
22260
22261            // remove previous draw
22262            if (shapeGroup) {
22263                shapeGroup.destroy();
22264            }
22265            // create shape
22266            var factory = new dwv.tool.RoiFactory();
22267            shapeGroup = factory.create(currentPath.pointArray, self.style);
22268            shapeGroup.id(dwv.math.guid());
22269            // draw shape command
22270            command = new dwv.tool.DrawGroupCommand(shapeGroup, "livewire", app.getCurrentDrawLayer());
22271            // draw
22272            command.execute();
22273        };
22274
22275        /**
22276         * Handle mouse up event.
22277         * @param {Object} event The mouse up event.
22278         */
22279        this.mouseup = function( /*event*/ ) {
22280            // nothing to do
22281        };
22282
22283        /**
22284         * Handle mouse out event.
22285         * @param {Object} event The mouse out event.
22286         */
22287        this.mouseout = function(event) {
22288            // treat as mouse up
22289            self.mouseup(event);
22290        };
22291
22292        /**
22293         * Handle double click event.
22294         * @param {Object} event The double click event.
22295         */
22296        this.dblclick = function( /*event*/ ) {
22297            console.log("dblclick");
22298            // save command in undo stack
22299            app.addToUndoStack(command);
22300            // set flag
22301            self.started = false;
22302        };
22303
22304        /**
22305         * Handle touch start event.
22306         * @param {Object} event The touch start event.
22307         */
22308        this.touchstart = function(event) {
22309            // treat as mouse down
22310            self.mousedown(event);
22311        };
22312
22313        /**
22314         * Handle touch move event.
22315         * @param {Object} event The touch move event.
22316         */
22317        this.touchmove = function(event) {
22318            // treat as mouse move
22319            self.mousemove(event);
22320        };
22321
22322        /**
22323         * Handle touch end event.
22324         * @param {Object} event The touch end event.
22325         */
22326        this.touchend = function(event) {
22327            // treat as mouse up
22328            self.mouseup(event);
22329        };
22330
22331        /**
22332         * Handle key down event.
22333         * @param {Object} event The key down event.
22334         */
22335        this.keydown = function(event) {
22336            app.onKeydown(event);
22337        };
22338
22339        /**
22340         * Setup the tool GUI.
22341         */
22342        this.setup = function() {
22343            gui = new dwv.gui.ColourTool(app, "lw");
22344            gui.setup();
22345        };
22346
22347        /**
22348         * Enable the tool.
22349         * @param {Boolean} bool The flag to enable or not.
22350         */
22351        this.display = function(bool) {
22352            if (gui) {
22353                gui.display(bool);
22354            }
22355            // start scissors if displayed
22356            if (bool) {
22357                //scissors = new dwv.math.Scissors();
22358                var size = app.getImage().getGeometry().getSize();
22359                scissors.setDimensions(
22360                    size.getNumberOfColumns(),
22361                    size.getNumberOfRows());
22362                scissors.setData(app.getImageData().data);
22363            }
22364        };
22365
22366        /**
22367         * Initialise the tool.
22368         */
22369        this.init = function() {
22370            if (gui) {
22371                // init with the app window scale
22372                this.style.setScale(app.getWindowScale());
22373                // set the default to the first in the list
22374                this.setLineColour(this.style.getLineColour());
22375                // init html
22376                gui.initialise();
22377            }
22378
22379            return true;
22380        };
22381
22382        /**
22383         * Add an event listener on the app.
22384         * @param {Object} listener The method associated with the provided event type.
22385         */
22386        this.addEventListener = function(listener) {
22387            listeners.push(listener);
22388        };
22389
22390        /**
22391         * Remove an event listener from the app.
22392         * @param {Object} listener The method associated with the provided event type.
22393         */
22394        this.removeEventListener = function(listener) {
22395            for (var i = 0; i < listeners.length; ++i) {
22396                if (listeners[i] === listener) {
22397                    listeners.splice(i, 1);
22398                }
22399            }
22400        };
22401
22402        // Private Methods -----------------------------------------------------------
22403
22404        /**
22405         * Fire an event: call all associated listeners.
22406         * @param {Object} event The event to fire.
22407         */
22408        function fireEvent(event) {
22409            for (var i = 0; i < listeners.length; ++i) {
22410                listeners[i](event);
22411            }
22412        }
22413
22414    }; // Livewire class
22415
22416    /**
22417     * Help for this tool.
22418     * @return {Object} The help content.
22419     */
22420    dwv.tool.Livewire.prototype.getHelp = function() {
22421        return {
22422            "title": dwv.i18n("tool.Livewire.name"),
22423            "brief": dwv.i18n("tool.Livewire.brief")
22424        };
22425    };
22426
22427    /**
22428     * Set the line colour of the drawing.
22429     * @param {String} colour The colour to set.
22430     */
22431    dwv.tool.Livewire.prototype.setLineColour = function(colour) {
22432        // set style var
22433        this.style.setLineColour(colour);
22434    };
22435
22436    // namespaces
22437    var dwv = dwv || {};
22438    dwv.tool = dwv.tool || {};
22439    // external
22440    var Konva = Konva || {};
22441
22442    /**
22443     * Protractor factory.
22444     * @constructor
22445     * @external Konva
22446     */
22447    dwv.tool.ProtractorFactory = function() {
22448        /**
22449         * Get the number of points needed to build the shape.
22450         * @return {Number} The number of points.
22451         */
22452        this.getNPoints = function() { return 3; };
22453        /**
22454         * Get the timeout between point storage.
22455         * @return {Number} The timeout in milliseconds.
22456         */
22457        this.getTimeout = function() { return 500; };
22458    };
22459
22460    /**
22461     * Create a protractor shape to be displayed.
22462     * @param {Array} points The points from which to extract the protractor.
22463     * @param {Object} style The drawing style.
22464     * @param {Object} image The associated image.
22465     */
22466    dwv.tool.ProtractorFactory.prototype.create = function(points, style /*, image*/ ) {
22467        // physical shape
22468        var line0 = new dwv.math.Line(points[0], points[1]);
22469        // points stored the Konvajs way
22470        var pointsArray = [];
22471        for (var i = 0; i < points.length; ++i) {
22472            pointsArray.push(points[i].getX());
22473            pointsArray.push(points[i].getY());
22474        }
22475        // draw shape
22476        var kshape = new Konva.Line({
22477            points: pointsArray,
22478            stroke: style.getLineColour(),
22479            strokeWidth: style.getScaledStrokeWidth(),
22480            name: "shape"
22481        });
22482        var group = new Konva.Group();
22483        group.name("protractor-group");
22484        group.add(kshape);
22485        group.visible(true); // dont inherit
22486        // text and decoration
22487        if (points.length === 3) {
22488            var line1 = new dwv.math.Line(points[1], points[2]);
22489            // larger hitfunc
22490            kshape.hitFunc(function(context) {
22491                context.beginPath();
22492                context.moveTo(points[0].getX(), points[0].getY());
22493                context.lineTo(points[1].getX(), points[1].getY());
22494                context.lineTo(points[2].getX(), points[2].getY());
22495                context.closePath();
22496                context.fillStrokeShape(this);
22497            });
22498            // quantification
22499            var angle = dwv.math.getAngle(line0, line1);
22500            var inclination = line0.getInclination();
22501            if (angle > 180) {
22502                angle = 360 - angle;
22503                inclination += angle;
22504            }
22505
22506            // quantification
22507            var quant = { "angle": { "value": angle, "unit": dwv.i18n("unit.degree") } };
22508            var ktext = new Konva.Text({
22509                fontSize: style.getScaledFontSize(),
22510                fontFamily: style.getFontFamily(),
22511                fill: style.getLineColour(),
22512                name: "text"
22513            });
22514            ktext.textExpr = "{angle}";
22515            ktext.longText = "";
22516            ktext.quant = quant;
22517            ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22518
22519            // label
22520            var midX = (line0.getMidpoint().getX() + line1.getMidpoint().getX()) / 2;
22521            var midY = (line0.getMidpoint().getY() + line1.getMidpoint().getY()) / 2;
22522            var klabel = new Konva.Label({
22523                x: midX,
22524                y: midY - 15,
22525                name: "label"
22526            });
22527            klabel.add(ktext);
22528            klabel.add(new Konva.Tag());
22529
22530            // arc
22531            var radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100;
22532            var karc = new Konva.Arc({
22533                innerRadius: radius,
22534                outerRadius: radius,
22535                stroke: style.getLineColour(),
22536                strokeWidth: style.getScaledStrokeWidth(),
22537                angle: angle,
22538                rotation: -inclination,
22539                x: points[1].getX(),
22540                y: points[1].getY(),
22541                name: "shape-arc"
22542            });
22543            // add to group
22544            group.add(klabel);
22545            group.add(karc);
22546        }
22547        // return group
22548        return group;
22549    };
22550
22551    /**
22552     * Update a protractor shape.
22553     * @param {Object} anchor The active anchor.
22554     * @param {Object} image The associated image.
22555     */
22556    dwv.tool.UpdateProtractor = function(anchor /*, image*/ ) {
22557        // parent group
22558        var group = anchor.getParent();
22559        // associated shape
22560        var kline = group.getChildren(function(node) {
22561            return node.name() === 'shape';
22562        })[0];
22563        // associated label
22564        var klabel = group.getChildren(function(node) {
22565            return node.name() === 'label';
22566        })[0];
22567        // associated arc
22568        var karc = group.getChildren(function(node) {
22569            return node.name() === 'shape-arc';
22570        })[0];
22571        // find special points
22572        var begin = group.getChildren(function(node) {
22573            return node.id() === 'begin';
22574        })[0];
22575        var mid = group.getChildren(function(node) {
22576            return node.id() === 'mid';
22577        })[0];
22578        var end = group.getChildren(function(node) {
22579            return node.id() === 'end';
22580        })[0];
22581        // update special points
22582        switch (anchor.id()) {
22583            case 'begin':
22584                begin.x(anchor.x());
22585                begin.y(anchor.y());
22586                break;
22587            case 'mid':
22588                mid.x(anchor.x());
22589                mid.y(anchor.y());
22590                break;
22591            case 'end':
22592                end.x(anchor.x());
22593                end.y(anchor.y());
22594                break;
22595        }
22596        // update shape and compensate for possible drag
22597        // note: shape.position() and shape.size() won't work...
22598        var bx = begin.x() - kline.x();
22599        var by = begin.y() - kline.y();
22600        var mx = mid.x() - kline.x();
22601        var my = mid.y() - kline.y();
22602        var ex = end.x() - kline.x();
22603        var ey = end.y() - kline.y();
22604        kline.points([bx, by, mx, my, ex, ey]);
22605        // larger hitfunc
22606        kline.hitFunc(function(context) {
22607            context.beginPath();
22608            context.moveTo(bx, by);
22609            context.lineTo(mx, my);
22610            context.lineTo(ex, ey);
22611            context.closePath();
22612            context.fillStrokeShape(this);
22613        });
22614        // update text
22615        var p2d0 = new dwv.math.Point2D(begin.x(), begin.y());
22616        var p2d1 = new dwv.math.Point2D(mid.x(), mid.y());
22617        var p2d2 = new dwv.math.Point2D(end.x(), end.y());
22618        var line0 = new dwv.math.Line(p2d0, p2d1);
22619        var line1 = new dwv.math.Line(p2d1, p2d2);
22620        var angle = dwv.math.getAngle(line0, line1);
22621        var inclination = line0.getInclination();
22622        if (angle > 180) {
22623            angle = 360 - angle;
22624            inclination += angle;
22625        }
22626
22627        // update text
22628        var quant = { "angle": { "value": angle, "unit": dwv.i18n("unit.degree") } };
22629        var ktext = klabel.getText();
22630        ktext.quant = quant;
22631        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22632        // update position
22633        var midX = (line0.getMidpoint().getX() + line1.getMidpoint().getX()) / 2;
22634        var midY = (line0.getMidpoint().getY() + line1.getMidpoint().getY()) / 2;
22635        var textPos = { 'x': midX, 'y': midY - 15 };
22636        klabel.position(textPos);
22637
22638        // arc
22639        var radius = Math.min(line0.getLength(), line1.getLength()) * 33 / 100;
22640        karc.innerRadius(radius);
22641        karc.outerRadius(radius);
22642        karc.angle(angle);
22643        karc.rotation(-inclination);
22644        var arcPos = { 'x': mid.x(), 'y': mid.y() };
22645        karc.position(arcPos);
22646    };
22647
22648    // namespaces
22649    var dwv = dwv || {};
22650    dwv.tool = dwv.tool || {};
22651    // external
22652    var Konva = Konva || {};
22653
22654    /**
22655     * Rectangle factory.
22656     * @constructor
22657     * @external Konva
22658     */
22659    dwv.tool.RectangleFactory = function() {
22660        /**
22661         * Get the number of points needed to build the shape.
22662         * @return {Number} The number of points.
22663         */
22664        this.getNPoints = function() { return 2; };
22665        /**
22666         * Get the timeout between point storage.
22667         * @return {Number} The timeout in milliseconds.
22668         */
22669        this.getTimeout = function() { return 0; };
22670    };
22671
22672    /**
22673     * Create a rectangle shape to be displayed.
22674     * @param {Array} points The points from which to extract the rectangle.
22675     * @param {Object} style The drawing style.
22676     * @param {Object} image The associated image.
22677     */
22678    dwv.tool.RectangleFactory.prototype.create = function(points, style, image) {
22679        // physical shape
22680        var rectangle = new dwv.math.Rectangle(points[0], points[1]);
22681        // draw shape
22682        var kshape = new Konva.Rect({
22683            x: rectangle.getBegin().getX(),
22684            y: rectangle.getBegin().getY(),
22685            width: rectangle.getWidth(),
22686            height: rectangle.getHeight(),
22687            s
22687troke: style.getLineColour(),
22688            strokeWidth: style.getScaledStrokeWidth(),
22689            name: "shape"
22690        });
22691        // quantification
22692        var quant = image.quantifyRect(rectangle);
22693        var ktext = new Konva.Text({
22694            fontSize: style.getScaledFontSize(),
22695            fontFamily: style.getFontFamily(),
22696            fill: style.getLineColour(),
22697            name: "text"
22698        });
22699        ktext.textExpr = "{surface}";
22700        ktext.longText = "";
22701        ktext.quant = quant;
22702        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22703
22704        // label
22705        var klabel = new Konva.Label({
22706            x: rectangle.getBegin().getX(),
22707            y: rectangle.getEnd().getY() + 10,
22708            name: "label"
22709        });
22710        klabel.add(ktext);
22711        klabel.add(new Konva.Tag());
22712
22713        // return group
22714        var group = new Konva.Group();
22715        group.name("rectangle-group");
22716        group.add(kshape);
22717        group.add(klabel);
22718        group.visible(true); // dont inherit
22719        return group;
22720    };
22721
22722    /**
22723     * Update a rectangle shape.
22724     * @param {Object} anchor The active anchor.
22725     * @param {Object} image The associated image.
22726     */
22727    dwv.tool.UpdateRect = function(anchor, image) {
22728        // parent group
22729        var group = anchor.getParent();
22730        // associated shape
22731        var krect = group.getChildren(function(node) {
22732            return node.name() === 'shape';
22733        })[0];
22734        // associated label
22735        var klabel = group.getChildren(function(node) {
22736            return node.name() === 'label';
22737        })[0];
22738        // find special points
22739        var topLeft = group.getChildren(function(node) {
22740            return node.id() === 'topLeft';
22741        })[0];
22742        var topRight = group.getChildren(function(node) {
22743            return node.id() === 'topRight';
22744        })[0];
22745        var bottomRight = group.getChildren(function(node) {
22746            return node.id() === 'bottomRight';
22747        })[0];
22748        var bottomLeft = group.getChildren(function(node) {
22749            return node.id() === 'bottomLeft';
22750        })[0];
22751        // update 'self' (undo case) and special points
22752        switch (anchor.id()) {
22753            case 'topLeft':
22754                topLeft.x(anchor.x());
22755                topLeft.y(anchor.y());
22756                topRight.y(anchor.y());
22757                bottomLeft.x(anchor.x());
22758                break;
22759            case 'topRight':
22760                topRight.x(anchor.x());
22761                topRight.y(anchor.y());
22762                topLeft.y(anchor.y());
22763                bottomRight.x(anchor.x());
22764                break;
22765            case 'bottomRight':
22766                bottomRight.x(anchor.x());
22767                bottomRight.y(anchor.y());
22768                bottomLeft.y(anchor.y());
22769                topRight.x(anchor.x());
22770                break;
22771            case 'bottomLeft':
22772                bottomLeft.x(anchor.x());
22773                bottomLeft.y(anchor.y());
22774                bottomRight.y(anchor.y());
22775                topLeft.x(anchor.x());
22776                break;
22777            default:
22778                console.error('Unhandled anchor id: ' + anchor.id());
22779                break;
22780        }
22781        // update shape
22782        krect.position(topLeft.position());
22783        var width = topRight.x() - topLeft.x();
22784        var height = bottomLeft.y() - topLeft.y();
22785        if (width && height) {
22786            krect.size({ 'width': width, 'height': height });
22787        }
22788        // new rect
22789        var p2d0 = new dwv.math.Point2D(topLeft.x(), topLeft.y());
22790        var p2d1 = new dwv.math.Point2D(bottomRight.x(), bottomRight.y());
22791        var rect = new dwv.math.Rectangle(p2d0, p2d1);
22792        // update text
22793        var quant = image.quantifyRect(rect);
22794        var ktext = klabel.getText();
22795        ktext.quant = quant;
22796        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22797        // update position
22798        var textPos = { 'x': rect.getBegin().getX(), 'y': rect.getEnd().getY() + 10 };
22799        klabel.position(textPos);
22800    };
22801
22802    // namespaces
22803    var dwv = dwv || {};
22804    dwv.tool = dwv.tool || {};
22805    // external
22806    var Konva = Konva || {};
22807
22808    /**
22809     * ROI factory.
22810     * @constructor
22811     * @external Konva
22812     */
22813    dwv.tool.RoiFactory = function() {
22814        /**
22815         * Get the number of points needed to build the shape.
22816         * @return {Number} The number of points.
22817         */
22818        this.getNPoints = function() { return 50; };
22819        /**
22820         * Get the timeout between point storage.
22821         * @return {Number} The timeout in milliseconds.
22822         */
22823        this.getTimeout = function() { return 100; };
22824    };
22825
22826    /**
22827     * Create a roi shape to be displayed.
22828     * @param {Array} points The points from which to extract the line.
22829     * @param {Object} style The drawing style.
22830     * @param {Object} image The associated image.
22831     */
22832    dwv.tool.RoiFactory.prototype.create = function(points, style /*, image*/ ) {
22833        // physical shape
22834        var roi = new dwv.math.ROI();
22835        // add input points to the ROI
22836        roi.addPoints(points);
22837        // points stored the Konvajs way
22838        var arr = [];
22839        for (var i = 0; i < roi.getLength(); ++i) {
22840            arr.push(roi.getPoint(i).getX());
22841            arr.push(roi.getPoint(i).getY());
22842        }
22843        // draw shape
22844        var kshape = new Konva.Line({
22845            points: arr,
22846            stroke: style.getLineColour(),
22847            strokeWidth: style.getScaledStrokeWidth(),
22848            name: "shape",
22849            closed: true
22850        });
22851
22852        // text
22853        var ktext = new Konva.Text({
22854            fontSize: style.getScaledFontSize(),
22855            fontFamily: style.getFontFamily(),
22856            fill: style.getLineColour(),
22857            name: "text"
22858        });
22859        ktext.textExpr = "";
22860        ktext.longText = "";
22861        ktext.quant = null;
22862        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22863
22864        // label
22865        var klabel = new Konva.Label({
22866            x: roi.getPoint(0).getX(),
22867            y: roi.getPoint(0).getY() + 10,
22868            name: "label"
22869        });
22870        klabel.add(ktext);
22871        klabel.add(new Konva.Tag());
22872
22873        // return group
22874        var group = new Konva.Group();
22875        group.name("roi-group");
22876        group.add(kshape);
22877        group.add(klabel);
22878        group.visible(true); // dont inherit
22879        return group;
22880    };
22881
22882    /**
22883     * Update a roi shape.
22884     * @param {Object} anchor The active anchor.
22885     * @param {Object} image The associated image.
22886     */
22887    dwv.tool.UpdateRoi = function(anchor /*, image*/ ) {
22888        // parent group
22889        var group = anchor.getParent();
22890        // associated shape
22891        var kroi = group.getChildren(function(node) {
22892            return node.name() === 'shape';
22893        })[0];
22894        // associated label
22895        var klabel = group.getChildren(function(node) {
22896            return node.name() === 'label';
22897        })[0];
22898
22899        // update self
22900        var point = group.getChildren(function(node) {
22901            return node.id() === anchor.id();
22902        })[0];
22903        point.x(anchor.x());
22904        point.y(anchor.y());
22905        // update the roi point and compensate for possible drag
22906        // (the anchor id is the index of the point in the list)
22907        var points = kroi.points();
22908        points[anchor.id()] = anchor.x() - kroi.x();
22909        points[anchor.id() + 1] = anchor.y() - kroi.y();
22910        kroi.points(points);
22911
22912        // update text
22913        var ktext = klabel.getText();
22914        ktext.quant = null;
22915        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
22916        // update position
22917        var textPos = { 'x': points[0] + kroi.x(), 'y': points[1] + kroi.y() + 10 };
22918        klabel.position(textPos);
22919
22920    };
22921
22922    // namespaces
22923    var dwv = dwv || {};
22924    dwv.tool = dwv.tool || {};
22925    // external
22926    var Konva = Konva || {};
22927
22928    /**
22929     * Ruler factory.
22930     * @constructor
22931     * @external Konva
22932     */
22933    dwv.tool.RulerFactory = function() {
22934        /**
22935         * Get the number of points needed to build the shape.
22936         * @return {Number} The number of points.
22937         */
22938        this.getNPoints = function() { return 2; };
22939        /**
22940         * Get the timeout between point storage.
22941         * @return {Number} The timeout in milliseconds.
22942         */
22943        this.getTimeout = function() { return 0; };
22944    };
22945
22946    /**
22947     * Create a ruler shape to be displayed.
22948     * @param {Array} points The points from which to extract the line.
22949     * @param {Object} style The drawing style.
22950     * @param {Object} image The associated image.
22951     */
22952    dwv.tool.RulerFactory.prototype.create = function(points, style, image) {
22953        // physical shape
22954        var line = new dwv.math.Line(points[0], points[1]);
22955        // draw shape
22956        var kshape = new Konva.Line({
22957            points: [line.getBegin().getX(), line.getBegin().getY(),
22958                line.getEnd().getX(), line.getEnd().getY()
22959            ],
22960            stroke: style.getLineColour(),
22961            strokeWidth: style.getScaledStrokeWidth(),
22962            name: "shape"
22963        });
22964
22965        // tick begin
22966        var linePerp0 = dwv.math.getPerpendicularLine(line, points[0], 10);
22967        var ktick0 = new Konva.Line({
22968            points: [linePerp0.getBegin().getX(), linePerp0.getBegin().getY(),
22969                linePerp0.getEnd().getX(), linePerp0.getEnd().getY()
22970            ],
22971            stroke: style.getLineColour(),
22972            strokeWidth: style.getScaledStrokeWidth(),
22973            name: "shape-tick0"
22974        });
22975
22976        // tick end
22977        var linePerp1 = dwv.math.getPerpendicularLine(line, points[1], 10);
22978        var ktick1 = new Konva.Line({
22979            points: [linePerp1.getBegin().getX(), linePerp1.getBegin().getY(),
22980                linePerp1.getEnd().getX(), linePerp1.getEnd().getY()
22981            ],
22982            stroke: style.getLineColour(),
22983            strokeWidth: style.getScaledStrokeWidth(),
22984            name: "shape-tick1"
22985        });
22986
22987        // larger hitfunc
22988        kshape.hitFunc(function(context) {
22989            context.beginPath();
22990            context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
22991            context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
22992            context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
22993            context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
22994            context.closePath();
22995            context.fillStrokeShape(this);
22996        });
22997
22998        // quantification
22999        var quant = image.quantifyLine(line);
23000        var ktext = new Konva.Text({
23001            fontSize: style.getScaledFontSize(),
23002            fontFamily: style.getFontFamily(),
23003            fill: style.getLineColour(),
23004            name: "text"
23005        });
23006        ktext.textExpr = "{length}";
23007        ktext.longText = "";
23008        ktext.quant = quant;
23009        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
23010        // label
23011        var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
23012        var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5;
23013        var klabel = new Konva.Label({
23014            x: line.getEnd().getX() + dX * 25,
23015            y: line.getEnd().getY() + dY * 15,
23016            name: "label"
23017        });
23018        klabel.add(ktext);
23019        klabel.add(new Konva.Tag());
23020
23021        // return group
23022        var group = new Konva.Group();
23023        group.name("ruler-group");
23024        group.add(kshape);
23025        group.add(ktick0);
23026        group.add(ktick1);
23027        group.add(klabel);
23028        group.visible(true); // dont inherit
23029        return group;
23030    };
23031
23032    /**
23033     * Update a ruler shape.
23034     * @param {Object} anchor The active anchor.
23035     * @param {Object} image The associated image.
23036     */
23037    dwv.tool.UpdateRuler = function(anchor, image) {
23038        // parent group
23039        var group = anchor.getParent();
23040        // associated shape
23041        var kline = group.getChildren(function(node) {
23042            return node.name() === 'shape';
23043        })[0];
23044        // associated tick0
23045        var ktick0 = group.getChildren(function(node) {
23046            return node.name() === 'shape-tick0';
23047        })[0];
23048        // associated tick1
23049        var ktick1 = group.getChildren(function(node) {
23050            return node.name() === 'shape-tick1';
23051        })[0];
23052        // associated label
23053        var klabel = group.getChildren(function(node) {
23054            return node.name() === 'label';
23055        })[0];
23056        // find special points
23057        var begin = group.getChildren(function(node) {
23058            return node.id() === 'begin';
23059        })[0];
23060        var end = group.getChildren(function(node) {
23061            return node.id() === 'end';
23062        })[0];
23063        // update special points
23064        switch (anchor.id()) {
23065            case 'begin':
23066                begin.x(anchor.x());
23067                begin.y(anchor.y());
23068                break;
23069            case 'end':
23070                end.x(anchor.x());
23071                end.y(anchor.y());
23072                break;
23073        }
23074        // update shape and compensate for possible drag
23075        // note: shape.position() and shape.size() won't work...
23076        var bx = begin.x() - kline.x();
23077        var by = begin.y() - kline.y();
23078        var ex = end.x() - kline.x();
23079        var ey = end.y() - kline.y();
23080        kline.points([bx, by, ex, ey]);
23081        // new line
23082        var p2d0 = new dwv.math.Point2D(begin.x(), begin.y());
23083        var p2d1 = new dwv.math.Point2D(end.x(), end.y());
23084        var line = new dwv.math.Line(p2d0, p2d1);
23085        // tick
23086        var p2b = new dwv.math.Point2D(bx, by);
23087        var p2e = new dwv.math.Point2D(ex, ey);
23088        var linePerp0 = dwv.math.getPerpendicularLine(line, p2b, 10);
23089        ktick0.points([linePerp0.getBegin().getX(), linePerp0.getBegin().getY(),
23090            linePerp0.getEnd().getX(), linePerp0.getEnd().getY()
23091        ]);
23092        var linePerp1 = dwv.math.getPerpendicularLine(line, p2e, 10);
23093        ktick1.points([linePerp1.getBegin().getX(), linePerp1.getBegin().getY(),
23094            linePerp1.getEnd().getX(), linePerp1.getEnd().getY()
23095        ]);
23096        // larger hitfunc
23097        kline.hitFunc(function(context) {
23098            context.beginPath();
23099            context.moveTo(linePerp0.getBegin().getX(), linePerp0.getBegin().getY());
23100            context.lineTo(linePerp0.getEnd().getX(), linePerp0.getEnd().getY());
23101            context.lineTo(linePerp1.getEnd().getX(), linePerp1.getEnd().getY());
23102            context.lineTo(linePerp1.getBegin().getX(), linePerp1.getBegin().getY());
23103            context.closePath();
23104            context.fillStrokeShape(this);
23105        });
23106        // update text
23107        var quant = image.quantifyLine(line);
23108        var ktext = klabel.getText();
23109        ktext.quant = quant;
23110        ktext.setText(dwv.utils.replaceFlags(ktext.textExpr, ktext.quant));
23111        // update position
23112        var dX = line.getBegin().getX() > line.getEnd().getX() ? 0 : -1;
23113        var dY = line.getBegin().getY() > line.getEnd().getY() ? -1 : 0.5;
23114        var textPos = {
23115            'x': line.getEnd().getX() + dX * 25,
23116            'y': line.getEnd().getY() + dY * 15
23117        };
23118        klabel.position(textPos);
23119    };
23120
23121    // namespaces
23122    var dwv = dwv || {};
23123    dwv.tool = dwv.tool || {};
23124
23125    /**
23126     * Scroll class.
23127     * @constructor
23128     * @param {Object} app The associated application.
23129     */
23130    dwv.tool.Scroll = function(app) {
23131        /**
23132         * Closure to self: to be used by event handlers.
23133         * @private
23134         * @type WindowLevel
23135         */
23136        var self = this;
23137        /**
23138         * Scroll GUI.
23139         * @type Object
23140         */
23141        var gui = null;
23142        /**
23143         * Interaction start flag.
23144         * @type Boolean
23145         */
23146        this.started = false;
23147        // touch timer ID (created by setTimeout)
23148        var touchTimerID = null;
23149
23150        /**
23151         * Handle mouse down event.
23152         * @param {Object} event The mouse down event.
23153         */
23154        this.mousedown = function(event) {
23155            // stop viewer if playing
23156            if (app.getViewController().isPlaying()) {
23157                app.getViewController().stop();
23158            }
23159            // start flag
23160            self.started = true;
23161            // first position
23162            self.x0 = event._x;
23163            self.y0 = event._y;
23164        };
23165
23166        /**
23167         * Handle mouse move event.
23168         * @param {Object} event The mouse move event.
23169         */
23170        this.mousemove = function(event) {
23171            if (!self.started) {
23172                return;
23173            }
23174
23175            // difference to last Y position
23176            var diffY = event._y - self.y0;
23177            var yMove = (Math.abs(diffY) > 15);
23178            // do not trigger for small moves
23179            if (yMove) {
23180                // update GUI
23181                if (diffY > 0) {
23182                    app.getViewController().decrementSliceNb();
23183                } else {
23184                    app.getViewController().incrementSliceNb();
23185                }
23186            }
23187
23188            // difference to last X position
23189            var diffX = event._x - self.x0;
23190            var xMove = (Math.abs(diffX) > 15);
23191            // do not trigger for small moves
23192            if (xMove) {
23193                // update GUI
23194                if (diffX > 0) {
23195                    app.getViewController().incrementFrameNb();
23196                } else {
23197                    app.getViewController().decrementFrameNb();
23198                }
23199            }
23200
23201            // reset origin point
23202            if (xMove) {
23203                self.x0 = event._x;
23204            }
23205            if (yMove) {
23206                self.y0 = event._y;
23207            }
23208        };
23209
23210        /**
23211         * Handle mouse up event.
23212         * @param {Object} event The mouse up event.
23213         */
23214        this.mouseup = function( /*event*/ ) {
23215            if (self.started) {
23216                // stop recording
23217                self.started = false;
23218            }
23219        };
23220
23221        /**
23222         * Handle mouse out event.
23223         * @param {Object} event The mouse out event.
23224         */
23225        this.mouseout = function(event) {
23226            self.mouseup(event);
23227        };
23228
23229        /**
23230         * Handle touch start event.
23231         * @param {Object} event The touch start event.
23232         */
23233        this.touchstart = function(event) {
23234            // long touch triggers the dblclick
23235            touchTimerID = setTimeout(self.dblclick, 500);
23236            // call mouse equivalent
23237            self.mousedown(event);
23238        };
23239
23240        /**
23241         * Handle touch move event.
23242         * @param {Object} event The touch move event.
23243         */
23244        this.touchmove = function(event) {
23245            // abort timer if move
23246            if (touchTimerID !== null) {
23247                clearTimeout(touchTimerID);
23248                touchTimerID = null;
23249            }
23250            // call mouse equivalent
23251            self.mousemove(event);
23252        };
23253
23254        /**
23255         * Handle touch end event.
23256         * @param {Object} event The touch end event.
23257         */
23258        this.touchend = function(event) {
23259            // abort timer
23260            if (touchTimerID !== null) {
23261                clearTimeout(touchTimerID);
23262                touchTimerID = null;
23263            }
23264            // call mouse equivalent
23265            self.mouseup(event);
23266        };
23267
23268        /**
23269         * Handle mouse scroll event (fired by Firefox).
23270         * @param {Object} event The mouse scroll event.
23271         */
23272        this.DOMMouseScroll = function(event) {
23273            // ev.detail on firefox is 3
23274            if (event.detail < 0) {
23275                mouseScroll(true);
23276            } else {
23277                mouseScroll(false);
23278            }
23279        };
23280
23281        /**
23282         * Handle mouse wheel event.
23283         * @param {Object} event The mouse wheel event.
23284         */
23285        this.mousewheel = function(event) {
23286            // ev.wheelDelta on chrome is 120
23287            if (event.wheelDelta > 0) {
23288                mouseScroll(true);
23289            } else {
23290                mouseScroll(false);
23291            }
23292        };
23293
23294        /**
23295         * Mouse scroll action.
23296         * @param {Boolean} up True to increment, false to decrement.
23297         */
23298        function mouseScroll(up) {
23299            var hasSlices = (app.getImage().getGeometry().getSize().getNumberOfSlices() !== 1);
23300            var hasFrames = (app.getImage().getNumberOfFrames() !== 1);
23301            if (up) {
23302                if (hasSlices) {
23303                    app.getViewController().incrementSliceNb();
23304                } else if (hasFrames) {
23305                    app.getViewController().incrementFrameNb();
23306                }
23307            } else {
23308                if (hasSlices) {
23309                    app.getViewController().decrementSliceNb();
23310                } else if (hasFrames) {
23311         
23311           app.getViewController().decrementFrameNb();
23312                }
23313            }
23314        }
23315
23316        /**
23317         * Handle key down event.
23318         * @param {Object} event The key down event.
23319         */
23320        this.keydown = function(event) {
23321            app.onKeydown(event);
23322        };
23323        /**
23324         * Handle double click.
23325         * @param {Object} event The key down event.
23326         */
23327        this.dblclick = function( /*event*/ ) {
23328            app.getViewController().play();
23329        };
23330
23331        /**
23332         * Setup the tool GUI.
23333         */
23334        this.setup = function() {
23335            gui = new dwv.gui.Scroll(app);
23336            gui.setup();
23337        };
23338
23339        /**
23340         * Enable the tool.
23341         * @param {Boolean} bool The flag to enable or not.
23342         */
23343        this.display = function(bool) {
23344            if (gui) {
23345                gui.display(bool);
23346            }
23347        };
23348
23349        /**
23350         * Initialise the tool.
23351         */
23352        this.init = function() {
23353            if (app.isMonoSliceData() && app.getImage().getNumberOfFrames() === 1) {
23354                return false;
23355            }
23356            return true;
23357        };
23358
23359    }; // Scroll class
23360
23361    /**
23362     * Help for this tool.
23363     * @return {Object} The help content.
23364     */
23365    dwv.tool.Scroll.prototype.getHelp = function() {
23366        return {
23367            "title": dwv.i18n("tool.Scroll.name"),
23368            "brief": dwv.i18n("tool.Scroll.brief"),
23369            "mouse": {
23370                "mouse_drag": dwv.i18n("tool.Scroll.mouse_drag"),
23371                "double_click": dwv.i18n("tool.Scroll.double_click")
23372            },
23373            "touch": {
23374                'touch_drag': dwv.i18n("tool.Scroll.touch_drag"),
23375                'tap_and_hold': dwv.i18n("tool.Scroll.tap_and_hold")
23376            }
23377        };
23378    };
23379
23380    // namespaces
23381    var dwv = dwv || {};
23382    dwv.tool = dwv.tool || {};
23383
23384    /**
23385     * Tool box.
23386     * @constructor
23387     * @param {Array} toolList The list of tool objects.
23388     * @param {Object} gui The associated gui.
23389     */
23390    dwv.tool.Toolbox = function(toolList, app) {
23391        /**
23392         * Toolbox GUI.
23393         * @type Object
23394         */
23395        var gui = null;
23396        /**
23397         * Selected tool.
23398         * @type Object
23399         */
23400        var selectedTool = null;
23401        /**
23402         * Default tool name.
23403         * @type String
23404         */
23405        var defaultToolName = null;
23406
23407        /**
23408         * Get the list of tools.
23409         * @return {Array} The list of tool objects.
23410         */
23411        this.getToolList = function() {
23412            return toolList;
23413        };
23414
23415        /**
23416         * Get the selected tool.
23417         * @return {Object} The selected tool.
23418         */
23419        this.getSelectedTool = function() {
23420            return selectedTool;
23421        };
23422
23423        /**
23424         * Setup the toolbox GUI.
23425         */
23426        this.setup = function() {
23427            if (Object.keys(toolList).length !== 0) {
23428                gui = new dwv.gui.Toolbox(app);
23429                gui.setup(toolList);
23430
23431                for (var key in toolList) {
23432                    toolList[key].setup();
23433                }
23434            }
23435        };
23436
23437        /**
23438         * Display the toolbox.
23439         * @param {Boolean} bool Flag to display or not.
23440         */
23441        this.display = function(bool) {
23442            if (Object.keys(toolList).length !== 0 && gui) {
23443                gui.display(bool);
23444            }
23445        };
23446
23447        /**
23448         * Initialise the tool box.
23449         */
23450        this.init = function() {
23451            var keys = Object.keys(toolList);
23452            // check if we have tools
23453            if (keys.length === 0) {
23454                return;
23455            }
23456            // init all tools
23457            defaultToolName = "";
23458            var displays = [];
23459            var display = null;
23460            for (var key in toolList) {
23461                display = toolList[key].init();
23462                if (display && defaultToolName === "") {
23463                    defaultToolName = key;
23464                }
23465                displays.push(display);
23466            }
23467            this.setSelectedTool(defaultToolName);
23468            // init html
23469            if (gui) {
23470                gui.initialise(displays);
23471            }
23472        };
23473
23474        /**
23475         * Set the selected tool.
23476         * @return {String} The name of the tool to select.
23477         */
23478        this.setSelectedTool = function(name) {
23479            // check if we have it
23480            if (!this.hasTool(name)) {
23481                throw new Error("Unknown tool: '" + name + "'");
23482            }
23483            // hide last selected
23484            if (selectedTool) {
23485                selectedTool.display(false);
23486            }
23487            // enable new one
23488            selectedTool = toolList[name];
23489            // display it
23490            selectedTool.display(true);
23491        };
23492
23493        /**
23494         * Reset the tool box.
23495         */
23496        this.reset = function() {
23497            // hide last selected
23498            if (selectedTool) {
23499                selectedTool.display(false);
23500            }
23501            selectedTool = null;
23502            defaultToolName = null;
23503        };
23504    };
23505
23506    /**
23507     * Check if a tool is in the tool list.
23508     * @param {String} name The name to check.
23509     * @return {String} The tool list element for the given name.
23510     */
23511    dwv.tool.Toolbox.prototype.hasTool = function(name) {
23512        return this.getToolList()[name];
23513    };
23514
23515    // namespaces
23516    var dwv = dwv || {};
23517    dwv.tool = dwv.tool || {};
23518
23519    /**
23520     * UndoStack class.
23521     * @constructor
23522     */
23523    dwv.tool.UndoStack = function(app) {
23524        /**
23525         * Undo GUI.
23526         * @type Object
23527         */
23528        var gui = new dwv.gui.Undo(app);
23529        /**
23530         * Array of commands.
23531         * @private
23532         * @type Array
23533         */
23534        var stack = [];
23535
23536        /**
23537         * Get the stack.
23538         * @return {Array} The list of stored commands.
23539         */
23540        this.getStack = function() { return stack; };
23541
23542        /**
23543         * Current command index.
23544         * @private
23545         * @type Number
23546         */
23547        var curCmdIndex = 0;
23548
23549        /**
23550         * Add a command to the stack.
23551         * @param {Object} cmd The command to add.
23552         */
23553        this.add = function(cmd) {
23554            // clear commands after current index
23555            stack = stack.slice(0, curCmdIndex);
23556            // store command
23557            stack.push(cmd);
23558            //stack[curCmdIndex] = cmd;
23559            // increment index
23560            ++curCmdIndex;
23561            // add command to display history
23562            gui.addCommandToUndoHtml(cmd.getName());
23563        };
23564
23565        /**
23566         * Undo the last command.
23567         */
23568        this.undo = function() {
23569            // a bit inefficient...
23570            if (curCmdIndex > 0) {
23571                // decrement command index
23572                --curCmdIndex;
23573                // undo last command
23574                stack[curCmdIndex].undo();
23575                // disable last in display history
23576                gui.enableInUndoHtml(false);
23577            }
23578        };
23579
23580        /**
23581         * Redo the last command.
23582         */
23583        this.redo = function() {
23584            if (curCmdIndex < stack.length) {
23585                // run last command
23586                stack[curCmdIndex].execute();
23587                // increment command index
23588                ++curCmdIndex;
23589                // enable next in display history
23590                gui.enableInUndoHtml(true);
23591            }
23592        };
23593
23594        /**
23595         * Setup the tool GUI.
23596         */
23597        this.setup = function() {
23598            gui.setup();
23599        };
23600
23601        /**
23602         * Initialise the tool GUI.
23603         */
23604        this.initialise = function() {
23605            gui.initialise();
23606        };
23607
23608    }; // UndoStack class
23609
23610    // namespaces
23611    var dwv = dwv || {};
23612    dwv.tool = dwv.tool || {};
23613
23614    /**
23615     * WindowLevel tool: handle window/level related events.
23616     * @constructor
23617     * @param {Object} app The associated application.
23618     */
23619    dwv.tool.WindowLevel = function(app) {
23620        /**
23621         * Closure to self: to be used by event handlers.
23622         * @private
23623         * @type WindowLevel
23624         */
23625        var self = this;
23626        /**
23627         * WindowLevel GUI.
23628         * @type Object
23629         */
23630        var gui = null;
23631        /**
23632         * Interaction start flag.
23633         * @type Boolean
23634         */
23635        this.started = false;
23636
23637        /**
23638         * Handle mouse down event.
23639         * @param {Object} event The mouse down event.
23640         */
23641        this.mousedown = function(event) {
23642            // set start flag
23643            self.started = true;
23644            // store initial position
23645            self.x0 = event._x;
23646            self.y0 = event._y;
23647            // update GUI
23648            app.getViewController().setCurrentPosition2D(event._x, event._y);
23649        };
23650
23651        /**
23652         * Handle mouse move event.
23653         * @param {Object} event The mouse move event.
23654         */
23655        this.mousemove = function(event) {
23656            // check start flag
23657            if (!self.started) {
23658                return;
23659            }
23660            // difference to last position
23661            var diffX = event._x - self.x0;
23662            var diffY = self.y0 - event._y;
23663            // calculate new window level
23664            var windowCenter = parseInt(app.getViewController().getWindowLevel().center, 10) + diffY;
23665            var windowWidth = parseInt(app.getViewController().getWindowLevel().width, 10) + diffX;
23666
23667            // add the manual preset to the view
23668            app.getViewController().addWindowLevelPresets({
23669                "manual": {
23670                    "wl": new dwv.image.WindowLevel(windowCenter, windowWidth),
23671                    "name": "manual"
23672                }
23673            });
23674            app.getViewController().setWindowLevelPreset("manual");
23675
23676            // update gui
23677            if (gui) {
23678                // initialise to add the manual preset
23679                gui.initialise();
23680                // set selected preset
23681                dwv.gui.setSelected(app.getElement("presetSelect"), "manual");
23682            }
23683
23684            // store position
23685            self.x0 = event._x;
23686            self.y0 = event._y;
23687        };
23688
23689        /**
23690         * Handle mouse up event.
23691         * @param {Object} event The mouse up event.
23692         */
23693        this.mouseup = function( /*event*/ ) {
23694            // set start flag
23695            if (self.started) {
23696                self.started = false;
23697            }
23698        };
23699
23700        /**
23701         * Handle mouse out event.
23702         * @param {Object} event The mouse out event.
23703         */
23704        this.mouseout = function(event) {
23705            // treat as mouse up
23706            self.mouseup(event);
23707        };
23708
23709        /**
23710         * Handle touch start event.
23711         * @param {Object} event The touch start event.
23712         */
23713        this.touchstart = function(event) {
23714            self.mousedown(event);
23715        };
23716
23717        /**
23718         * Handle touch move event.
23719         * @param {Object} event The touch move event.
23720         */
23721        this.touchmove = function(event) {
23722            self.mousemove(event);
23723        };
23724
23725        /**
23726         * Handle touch end event.
23727         * @param {Object} event The touch end event.
23728         */
23729        this.touchend = function(event) {
23730            self.mouseup(event);
23731        };
23732
23733        /**
23734         * Handle double click event.
23735         * @param {Object} event The double click event.
23736         */
23737        this.dblclick = function(event) {
23738            // update GUI
23739            app.getViewController().setWindowLevel(
23740                parseInt(app.getImage().getRescaledValue(
23741                    event._x, event._y, app.getViewController().getCurrentPosition().k), 10),
23742                parseInt(app.getViewController().getWindowLevel().width, 10));
23743        };
23744
23745        /**
23746         * Handle key down event.
23747         * @param {Object} event The key down event.
23748         */
23749        this.keydown = function(event) {
23750            // let the app handle it
23751            app.onKeydown(event);
23752        };
23753
23754        /**
23755         * Setup the tool GUI.
23756         */
23757        this.setup = function() {
23758            gui = new dwv.gui.WindowLevel(app);
23759            gui.setup();
23760        };
23761
23762        /**
23763         * Display the tool.
23764         * @param {Boolean} bool The flag to display or not.
23765         */
23766        this.display = function(bool) {
23767            if (gui) {
23768                if (app.getImage().getPhotometricInterpretation().match(/MONOCHROME/) !== null) {
23769                    gui.display(bool);
23770                } else {
23771                    gui.display(false);
23772                }
23773            }
23774        };
23775
23776        /**
23777         * Initialise the tool.
23778         */
23779        this.init = function() {
23780            if (gui) {
23781                gui.initialise();
23782            }
23783            return true;
23784        };
23785    }; // WindowLevel class
23786
23787    /**
23788     * Help for this tool.
23789     * @return {Object} The help content.
23790     */
23791    dwv.tool.WindowLevel.prototype.getHelp = function() {
23792        return {
23793            "title": dwv.i18n("tool.WindowLevel.name"),
23794            "brief": dwv.i18n("tool.WindowLevel.brief"),
23795            "mouse": {
23796                "mouse_drag": dwv.i18n("tool.WindowLevel.mouse_drag"),
23797                "double_click": dwv.i18n("tool.WindowLevel.double_click")
23798            },
23799            "touch": {
23800                "touch_drag": dwv.i18n("tool.WindowLevel.touch_drag")
23801            }
23802        };
23803    };
23804
23805    // namespaces
23806    var dwv = dwv || {};
23807    dwv.tool = dwv.tool || {};
23808
23809    /**
23810     * ZoomAndPan class.
23811     * @constructor
23812     * @param {Object} app The associated application.
23813     */
23814    dwv.tool.ZoomAndPan = function(app) {
23815        /**
23816         * Closure to self: to be used by event handlers.
23817         * @private
23818         * @type Object
23819         */
23820        var self = this;
23821        /**
23822         * ZoomAndPan GUI.
23823         * @type Object
23824         */
23825        var gui = null;
23826        /**
23827         * Interaction start flag.
23828         * @type Boolean
23829         */
23830        this.started = false;
23831
23832        /**
23833         * Handle mouse down event.
23834         * @param {Object} event The mouse down event.
23835         */
23836        this.mousedown = function(event) {
23837            self.started = true;
23838            // first position
23839            self.x0 = event._xs;
23840            self.y0 = event._ys;
23841        };
23842
23843        /**
23844         * Handle two touch down event.
23845         * @param {Object} event The touch down event.
23846         */
23847        this.twotouchdown = function(event) {
23848            self.started = true;
23849            // store first point
23850            self.x0 = event._x;
23851            self.y0 = event._y;
23852            // first line
23853            var point0 = new dwv.math.Point2D(event._x, event._y);
23854            var point1 = new dwv.math.Point2D(event._x1, event._y1);
23855            self.line0 = new dwv.math.Line(point0, point1);
23856            self.midPoint = self.line0.getMidpoint();
23857        };
23858
23859        /**
23860         * Handle mouse move event.
23861         * @param {Object} event The mouse move event.
23862         */
23863        this.mousemove = function(event) {
23864            if (!self.started) {
23865                return;
23866            }
23867            // calculate translation
23868            var tx = event._xs - self.x0;
23869            var ty = event._ys - self.y0;
23870            // apply translation
23871            //app.translate(tx, ty);
23872            app.stepTranslate(tx, ty);
23873            // reset origin point
23874            self.x0 = event._xs;
23875            self.y0 = event._ys;
23876        };
23877
23878        /**
23879         * Handle two touch move event.
23880         * @param {Object} event The touch move event.
23881         */
23882        this.twotouchmove = function(event) {
23883            if (!self.started) {
23884                return;
23885            }
23886            var point0 = new dwv.math.Point2D(event._x, event._y);
23887            var point1 = new dwv.math.Point2D(event._x1, event._y1);
23888            var newLine = new dwv.math.Line(point0, point1);
23889            var lineRatio = newLine.getLength() / self.line0.getLength();
23890
23891            if (lineRatio === 1) {
23892                // scroll mode
23893                // difference  to last position
23894                var diffY = event._y - self.y0;
23895                // do not trigger for small moves
23896                if (Math.abs(diffY) < 15) {
23897                    return;
23898                }
23899                // update GUI
23900                if (diffY > 0) {
23901                    app.getViewController().incrementSliceNb();
23902                } else {
23903                    app.getViewController().decrementSliceNb();
23904                }
23905            } else {
23906                // zoom mode
23907                var zoom = (lineRatio - 1) / 2;
23908                if (Math.abs(zoom) % 0.1 <= 0.05) {
23909                    app.stepZoom(zoom, event._xs, event._ys);
23910                }
23911            }
23912        };
23913
23914        /**
23915         * Handle mouse up event.
23916         * @param {Object} event The mouse up event.
23917         */
23918        this.mouseup = function( /*event*/ ) {
23919            if (self.started) {
23920                // stop recording
23921                self.started = false;
23922            }
23923        };
23924
23925        /**
23926         * Handle mouse out event.
23927         * @param {Object} event The mouse out event.
23928         */
23929        this.mouseout = function(event) {
23930            self.mouseup(event);
23931        };
23932
23933        /**
23934         * Handle touch start event.
23935         * @param {Object} event The touch start event.
23936         */
23937        this.touchstart = function(event) {
23938            var touches = event.targetTouches;
23939            if (touches.length === 1) {
23940                self.mousedown(event);
23941            } else if (touches.length === 2) {
23942                self.twotouchdown(event);
23943            }
23944        };
23945
23946        /**
23947         * Handle touch move event.
23948         * @param {Object} event The touch move event.
23949         */
23950        this.touchmove = function(event) {
23951            var touches = event.targetTouches;
23952            if (touches.length === 1) {
23953                self.mousemove(event);
23954            } else if (touches.length === 2) {
23955                self.twotouchmove(event);
23956            }
23957        };
23958
23959        /**
23960         * Handle touch end event.
23961         * @param {Object} event The touch end event.
23962         */
23963        this.touchend = function(event) {
23964            self.mouseup(event);
23965        };
23966
23967        /**
23968         * Handle mouse scroll event (fired by Firefox).
23969         * @param {Object} event The mouse scroll event.
23970         */
23971        this.DOMMouseScroll = function(event) {
23972            // ev.detail on firefox is 3
23973            var step = -event.detail / 30;
23974            app.stepZoom(step, event._xs, event._ys);
23975        };
23976
23977        /**
23978         * Handle mouse wheel event.
23979         * @param {Object} event The mouse wheel event.
23980         */
23981        this.mousewheel = function(event) {
23982            // ev.wheelDelta on chrome is 120
23983            var step = event.wheelDelta / 1200;
23984            app.stepZoom(step, event._xs, event._ys);
23985        };
23986
23987        /**
23988         * Handle key down event.
23989         * @param {Object} event The key down event.
23990         */
23991        this.keydown = function(event) {
23992            app.onKeydown(event);
23993        };
23994
23995        /**
23996         * Setup the tool GUI.
23997         */
23998        this.setup = function() {
23999            gui = new dwv.gui.ZoomAndPan(app);
24000            gui.setup();
24001        };
24002
24003        /**
24004         * Enable the tool.
24005         * @param {Boolean} bool The flag to enable or not.
24006         */
24007        this.display = function(bool) {
24008            if (gui) {
24009                gui.display(bool);
24010            }
24011        };
24012
24013    }; // ZoomAndPan class
24014
24015    /**
24016     * Help for this tool.
24017     * @return {Object} The help content.
24018     */
24019    dwv.tool.ZoomAndPan.prototype.getHelp = function() {
24020        return {
24021            "title": dwv.i18n("tool.ZoomAndPan.name"),
24022            "brief": dwv.i18n("tool.ZoomAndPan.brief"),
24023            "mouse": {
24024                "mouse_wheel": dwv.i18n("tool.ZoomAndPan.mouse_wheel"),
24025                "mouse_drag": dwv.i18n("tool.ZoomAndPan.mouse_drag")
24026            },
24027            "touch": {
24028                'twotouch_pinch': dwv.i18n("tool.ZoomAndPan.twotouch_pinch"),
24029                'touch_drag': dwv.i18n("tool.ZoomAndPan.touch_drag")
24030            }
24031        };
24032    };
24033
24034    /**
24035     * Initialise the tool.
24036     */
24037    dwv.tool.ZoomAndPan.prototype.init = function() {
24038        return true;
24039    };
24040
24041    // namespaces
24042    var dwv = dwv || {};
24043    /** @namespace */
24044    dwv.browser = dwv.browser || {};
24045    // external
24046    var Modernizr = Modernizr || {};
24047
24048    /**
24049     * Browser check for the FileAPI.
24050     * Assume support for Safari5.
24051     */
24052    dwv.browser.hasFileApi = function() {
24053        // regular test does not work on Safari 5
24054        var isSafari5 = (navigator.appVersion.indexOf("Safari") !== -1) &&
24055            (navigator.appVersion.indexOf("Chrome") === -1) &&
24056            ((navigator.appVersion.indexOf("5.0.") !== -1) ||
24057                (navigator.appVersion.indexOf("5.1.") !== -1));
24058        if (isSafari5) {
24059            console.warn("Assuming FileAPI support for Safari5...");
24060            return true;
24061        }
24062        // regular test
24063        return Modernizr.filereader;
24064    };
24065
24066    /**
24067     * Browser check for the XMLHttpRequest.
24068     */
24069    dwv.browser.hasXmlHttpRequest = function() {
24070        return Modernizr.xhrresponsetype &&
24071            Modernizr.xhrresponsetypearraybuffer && Modernizr.xhrresponsetypetext &&
24072            "XMLHttpRequest" in window && "withCredentials" in new XMLHttpRequest();
24073    };
24074
24075    /**
24076     * Browser check for typed array.
24077     */
24078    dwv.browser.hasTypedArray = function() {
24079        return Modernizr.dataview && Modernizr.typedarrays;
24080    };
24081
24082    /**
24083     * Browser check for input with type='color'.
24084     * Missing in IE and Safari.
24085     */
24086    dwv.browser.hasInputColor = function() {
24087        return Modernizr.inputtypes.color;
24088    };
24089
24090    /**
24091     * Browser check for input with type='files' and webkitdirectory flag.
24092     * Missing in IE and Safari.
24093     */
24094    dwv.browser.hasInputDirectory = function() {
24095        return Modernizr.fileinputdirectory;
24096    };
24097
24098
24099    //only check at startup (since we propose a replacement)
24100    dwv.browser._hasTypedArraySlice = (typeof Uint8Array.prototype.slice !== "undefined");
24101
24102    /**
24103     * Browser check for typed array slice method.
24104     * Missing in Internet Explorer 11.
24105     */
24106    dwv.browser.hasTypedArraySlice = function() {
24107        return dwv.browser._hasTypedArraySlice;
24108    };
24109
24110    // only check at startup (since we propose a replacement)
24111    dwv.browser._hasFloat64Array = ("Float64Array" in window);
24112
24113    /**
24114     * Browser check for Float64Array array.
24115     * Missing in PhantomJS 1.9.20 (on Travis).
24116     */
24117    dwv.browser.hasFloat64Array = function() {
24118        return dwv.browser._hasFloat64Array;
24119    };
24120
24121    //only check at startup (since we propose a replacement)
24122    dwv.browser._hasClampedArray = ("Uint8ClampedArray" in window);
24123
24124    /**
24125     * Browser check for clamped array.
24126     * Missing in:
24127     * - Safari 5.1.7 for Windows
24128     * - PhantomJS 1.9.20 (on Travis).
24129     */
24130    dwv.browser.hasClampedArray = function() {
24131        return dwv.browser._hasClampedArray;
24132    };
24133
24134    /**
24135     * Browser checks to see if it can run dwv. Throws an error if not.
24136     * Silently replaces basic functions.
24137     */
24138    dwv.browser.check = function() {
24139
24140        // Required --------------
24141
24142        var appnorun = "The application cannot be run.";
24143        var message = "";
24144        // Check for the File API support
24145        if (!dwv.browser.hasFileApi()) {
24146            message = "The File APIs are not supported in this browser. ";
24147            alert(message + appnorun);
24148            throw new Error(message);
24149        }
24150        // Check for XMLHttpRequest
24151        if (!dwv.browser.hasXmlHttpRequest()) {
24152            message = "The XMLHttpRequest is not supported in this browser. ";
24153            alert(message + appnorun);
24154            throw new Error(message);
24155        }
24156        // Check typed array
24157        if (!dwv.browser.hasTypedArray()) {
24158            message = "The Typed arrays are not supported in this browser. ";
24159            alert(message + appnorun);
24160            throw new Error(message);
24161        }
24162
24163        // Replaced if not present ------------
24164
24165        // Check typed array slice
24166        if (!dwv.browser.hasTypedArraySlice()) {
24167            // silent fail with warning
24168            console.warn("The TypedArray.slice method is not supported in this browser. This may impair performance. ");
24169            // basic Uint16Array implementation
24170            Uint16Array.prototype.slice = function(begin, end) {
24171                var size = end - begin;
24172                var cloned = new Uint16Array(size);
24173                for (var i = 0; i < size; i++) {
24174                    cloned[i] = this[begin + i];
24175                }
24176                return cloned;
24177            };
24178            // basic Int16Array implementation
24179            Int16Array.prototype.slice = function(begin, end) {
24180                var size = end - begin;
24181                var cloned = new Int16Array(size);
24182                for (var i = 0; i < size; i++) {
24183                    cloned[i] = this[begin + i];
24184                }
24185                return cloned;
24186            };
24187            // basic Uint8Array implementation
24188            Uint8Array.prototype.slice = function(begin, end) {
24189                var size = end - begin;
24190                var cloned = new Uint8Array(size);
24191                for (var i = 0; i < size; i++) {
24192                    cloned[i] = this[begin + i];
24193                }
24194                return cloned;
24195            };
24196            // basic Int8Array implementation
24197            Int8Array.prototype.slice = function(begin, end) {
24198                var size = end - begin;
24199                var cloned = new Int8Array(size);
24200                for (var i = 0; i < size; i++) {
24201                    cloned[i] = this[begin + i];
24202                }
24203                return cloned;
24204            };
24205        }
24206        // check clamped array
24207        if (!dwv.browser.hasClampedArray()) {
24208            // silent fail with warning
24209            console.warn("The Uint8ClampedArray is not supported in this browser. This may impair performance. ");
24210            // Use Uint8Array instead... Not good
24211            // TODO Find better replacement!
24212            window.Uint8ClampedArray = window.Uint8Array;
24213        }
24214        // check Float64 array
24215        if (!dwv.browser.hasFloat64Array()) {
24216            // silent fail with warning
24217            console.warn("The Float64Array is not supported in this browser. This may impair performance. ");
24218            // Use Float32Array instead... Not good
24219            // TODO Find better replacement!
24220            window.Float64Array = window.Float32Array;
24221        }
24222    };
24223
24224    // namespaces
24225    var dwv = dwv || {};
24226    // external
24227    var i18next = i18next || {};
24228    var i18nextXHRBackend = i18nextXHRBackend || {};
24229    var i18nextBrowserLanguageDetector = i18nextBrowserLanguageDetector || {};
24230
24231    // This is mainly a wrapper around the i18next object.
24232    // see its API: http://i18next.com/docs/api/
24233
24234    // global locales path
24235    dwv.i18nLocalesPath = null;
24236
24237    /**
24238     * Initialise i18n.
24239     * @param {String} language The language to translate to. Defaults to 'auto' and
24240     *   gets the language from the browser.
24241     * @param {String} localesPath Path to the locales directory.
24242     * @external i18next
24243     * @external i18nextXHRBackend
24244     * @external i18nextBrowserLanguageDetector
24245     */
24246    dwv.i18nInitialise = function(language, localesPath) {
24247        var lng = (typeof language === "undefined") ? "auto" : language;
24248        var lpath = (typeof localesPath === "undefined") ? "../.." : localesPath;
24249        // store as global
24250        dwv.i18nLocalesPath = lpath;
24251        // i18n options: default 'en' language and
24252        //  only load language, not specialised (for ex en-GB)
24253        var options = {
24254            fallbackLng: "en",
24255            load: "languageOnly",
24256            backend: { loadPath: lpath + "/locales/{{lng}}/{{ns}}.json" }
24257        };
24258        // use the XHR backend to get translation files
24259        var i18n = i18next.use(i18nextXHRBackend);
24260        // use browser language or the specified one
24261        if (lng === "auto") {
24262            i18n.use(i18nextBrowserLanguageDetector);
24263        } else {
24264            options.lng = lng;
24265        }
24266        // init i18n: will be ready when the 'loaded' event is fired
24267        i18n.init(options);
24268    };
24269
24270    /**
24271     * Initialise i18n with recources as input.
24272     * @param {String} language The language to translate to. Defaults to 'auto' and
24273     *   gets the language from the browser.
24274     * @param {Object} resources Languages provided as object.
24275     * @external i18next
24276     * @external i18nextBrowserLanguageDetector
24277     */
24278    dwv.i18nInitialiseWithResources = function(language, resources) {
24279        var lng = (typeof language === "undefined") ? "auto" : language;
24280        // i18n options: default 'en' language and
24281        //  only load language, not specialised (for ex en-GB)
24282        var options = {
24283            fallbackLng: "en",
24284            load: "languageOnly",
24285            resources: resources
24286        };
24287        // use browser language or the specified one
24288        // init i18n: will be ready when the 'loaded' event is fired
24289        if (lng === "auto") {
24290            var i18n = i18next.use(i18nextBrowserLanguageDetector);
24291            i18n.init(options);
24292        } else {
24293            options.lng = lng;
24294            i18next.init(options);
24295        }
24296    };
24297
24298    /**
24299     * Handle i18n 'initialized' event.
24300     * @param {Object} callback The callback function to call when i18n is initialised.
24301     *  It can take one argument that will be replaced with the i18n options.
24302     * @external i18next
24303     */
24304    dwv.i18nOnInitialised = function(callback) {
24305        i18next.on('initialized', callback);
24306    };
24307
24308    /**
24309     * Stop handling i18n load event.
24310     * @external i18next
24311     */
24312    dwv.i18nOffInitialised = function() {
24313        i18next.off('initialized');
24314    };
24315
24316    /**
24317     * Handle i18n failed load event.
24318     * @param {Object} callback The callback function to call when i18n is loaded.
24319     *  It can take three arguments: lng, ns and msg.
24320     * @external i18next
24321     */
24322    dwv.i18nOnFailedLoad = function(callback) {
24323        i18next.on('failedLoading', callback);
24324    };
24325
24326    /**
24327     * Stop handling i18n failed load event.
24328     * @external i18next
24329     */
24330    dwv.i18nOffFailedLoad = function() {
24331        i18next.off('failedLoading');
24332    };
24333
24334    /**
24335     * Get the translated text.
24336     * @param {String} key The key to the text entry.
24337     * @param {Object} options The translation options such as plural, context...
24338     * @external i18next
24339     */
24340    dwv.i18n = function(key, options) {
24341        return i18next.t(key, options);
24342    };
24343
24344    /**
24345     * Check the existence of a translation.
24346     * @param {String} key The key to the text entry.
24347     * @param {Object} options The translation options such as plural, context...
24348     * @external i18next
24349     */
24350    dwv.i18nExists = function(key, options) {
24351        return i18next.exists(key, options);
24352    };
24353
24354    /**
24355     * Translate all data-i18n tags in the current html page. If an html tag defines the
24356     * data-i18n attribute, its value will be used as key to find its corresponding text
24357     * and will replace the content of the html tag.
24358     */
24359    dwv.i18nPage = function() {
24360        // get all elements
24361        var elements = document.getElementsByTagName("*");
24362        // if the element defines data-i18n, replace its content with the tranlation
24363        for (var i = 0; i < elements.length; ++i) {
24364            if (typeof elements[i].dataset.i18n !== "undefined") {
24365                elements[i].innerHTML = dwv.i18n(elements[i].dataset.i18n);
24366            }
24367        }
24368    };
24369
24370    /**
24371     * Get the current locale resource path.
24372     * Warning: to be used once i18next is initialised.
24373     * @return {String} The path to the locale resource.
24374     */
24375    dwv.i18nGetLocalePath = function(filename) {
24376        var lng = i18next.language.substr(0, 2);
24377        return dwv.i18nLocalesPath +
24378            "/locales/" + lng + "/" + filename;
24379    };
24380
24381    /**
24382     * Get the current locale resource path.
24383     * Warning: to be used once i18next is initialised.
24384     * @return {String} The path to the locale resource.
24385     */
24386    dwv.i18nGetFallbackLocalePath = function(filename) {
24387        var lng = i18next.languages[i18next.languages.length - 1].substr(0, 2);
24388        return dwv.i18nLocalesPath +
24389            "/locales/" + lng + "/" + filename;
24390    };
24391
24392    // namespaces
24393    var dwv = dwv || {};
24394    dwv.utils = dwv.utils || {};
24395
24396    /**
24397     * ListenerHandler class: handles add/removing and firing listeners.
24398     * @constructor
24399     */
24400    dwv.utils.ListenerHandler = function() {
24401        /**
24402         * listeners.
24403         * @private
24404         */
24405        var listeners = {};
24406
24407        /**
24408         * Add an event listener.
24409         * @param {String} type The event type.
24410         * @param {Object} callback The method associated with the provided event type,
24411         *    will be called with the fired event.
24412         */
24413        this.add = function(type, callback) {
24414            // create array if not present
24415            if (typeof listeners[type] === "undefined") {
24416                listeners[type] = [];
24417            }
24418            // add callback to listeners array
24419            listeners[type].push(callback);
24420        };
24421
24422        /**
24423         * Remove an event listener.
24424         * @param {String} type The event type.
24425         * @param {Object} callback The method associated with the provided event type.
24426         */
24427        this.remove = function(type, callback) {
24428            // check if the type is present
24429            if (typeof listeners[type] === "undefined") {
24430                return;
24431            }
24432            // remove from listeners array
24433            for (var i = 0; i < listeners[type].length; ++i) {
24434                if (listeners[type][i] === callback) {
24435                    listeners[type].splice(i, 1);
24436                }
24437            }
24438        };
24439
24440        /**
24441         * Fire an event: call all associated listeners with the input event object.
24442         * @param {Object} event The event to fire.
24443         */
24444        this.fireEvent = function(event) {
24445            // check if they are listeners for the event type
24446            if (typeof listeners[event.type] === "undefined") {
24447                return;
24448            }
24449            // fire events
24450            for (var i = 0; i < listeners[event.type].length; ++i) {
24451                listeners[event.type][i](event);
24452            }
24453        };
24454    };
24455
24456    // namespaces
24457    var dwv = dwv || {};
24458    dwv.utils = dwv.utils || {};
24459
24460    /**
24461     * Multiple progresses handler.
24462     * Stores a multi dimensional list of progresses to allow to
24463     * calculate a global progress.
24464     * @param {Function} callback The function to pass the global progress to.
24465     */
24466    dwv.utils.MultiProgressHandler = function(callback) {
24467        // closure to self
24468        var self = this;
24469
24470        /**
24471         * List of progresses.
24472         * @private
24473         * @type Array
24474         */
24475        var progresses = [];
24476
24477        /**
24478         * Number of dimensions.
24479         * @private
24480         * @type Number
24481         */
24482        var numberOfDimensions = 2;
24483
24484        /**
24485         * Set the number of dimensions.
24486         * @param {Number} num The number.
24487         */
24488        this.setNumberOfDimensions = function(num) {
24489            numberOfDimensions = num;
24490        };
24491
24492        /**
24493         * Set the number of data to load.
24494         * @param {Number} n The number of data to load.
24495         */
24496        this.setNToLoad = function(n) {
24497            for (var i = 0; i < n; ++i) {
24498                progresses[i] = [];
24499                for (var j = 0; j < numberOfDimensions; ++j) {
24500                    progresses[i][j] = 0;
24501                }
24502            }
24503        };
24504
24505        /**
24506         * Handle a load progress.
24507         * @param {Object} event The progress event.
24508         */
24509        this.onprogress = function(event) {
24510            // check event
24511            if (!event.lengthComputable) {
24512                return;
24513            }
24514            if (typeof event.subindex === "undefined") {
24515                return;
24516            }
24517            if (typeof event.index === "undefined") {
24518                return;
24519            }
24520            // calculate percent
24521            var percent = (event.loaded * 100) / event.total;
24522            // set percent for index
24523            progresses[event.index][event.subindex] = percent;
24524
24525            // call callback
24526            callback({
24527                'type': event.type,
24528                'lengthComputable': true,
24529                'loaded': getGlobalPercent(),
24530                'total': 100
24531            });
24532        };
24533
24534        /**
24535         * Get the global load percent including the provided one.
24536         * @return {Number} The accumulated percentage.
24537         */
24538        function getGlobalPercent() {
24539            var sum = 0;
24540            var lenprog = progresses.length;
24541            for (var i = 0; i < lenprog; ++i) {
24542                for (var j = 0; j < numberOfDimensions; ++j) {
24543                    sum += progresses[i][j];
24544                }
24545            }
24546            return Math.round(sum / (lenprog * numberOfDimensions));
24547        }
24548
24549        /**
24550         * Create a mono progress event handler.
24551         * @param {Number} index The index of the data.
24552         * @param {Number} subindex The sub-index of the data.
24553         */
24554        this.getMonoProgressHandler = function(index, subindex) {
24555            return function(event) {
24556                event.index = index;
24557                event.subindex = subindex;
24558                self.onprogress(event);
24559            };
24560        };
24561
24562        /**
24563         * Create a mono loadend event handler: sends a 100% progress.
24564         * @param {Number} index The index of the data.
24565         * @param {Number} subindex The sub-index of the data.
24566         */
24567        this.getMonoOnLoadEndHandler = function(index, subindex) {
24568            return function() {
24569                self.onprogress({
24570                    'type': 'load-progress',
24571                    'lengthComputable': true,
24572                    'loaded': 100,
24573                    'total': 100,
24574                    'index': index,
24575                    'subindex': subindex
24576                });
24577            };
24578        };
24579
24580        /**
24581         * Create a mono progress event handler with an undefined index.
24582         * Warning: The caller handles the progress index.
24583         * @param {Number} subindex The sub-index of the data.
24584         */
24585        this.getUndefinedMonoProgressHandler = function(subindex) {
24586            return function(event) {
24587                event.subindex = subindex;
24588                self.onprogress(event);
24589            };
24590        };
24591    };
24592
24593    // namespaces
24594    var dwv = dwv || {};
24595    /** @namespace */
24596    dwv.utils = dwv.utils || {};
24597
24598    /**
24599     * Capitalise the first letter of a string.
24600     * @param {String} string The string to capitalise the first letter.
24601     * @return {String} The new string.
24602     */
24603    dwv.utils.capitaliseFirstLetter = function(string) {
24604        var res = string;
24605        if (string) {
24606            res = string.charAt(0).toUpperCase() + string.slice(1);
24607        }
24608        return res;
24609    };
24610
24611    /**
24612     * Split key/value string:
24613     *  key0=val00&key0=val01&key1=val10 returns
24614     *  { key0 : [val00, val01], key1 : val1 }
24615     * @param {String} inputStr The string to split.
24616     * @return {Object} The split string.
24617     */
24618    dwv.utils.splitKeyValueString = function(inputStr) {
24619        // result
24620        var result = {};
24621        // check input string
24622        if (inputStr) {
24623            // split key/value pairs
24624            var pairs = inputStr.split('&');
24625            for (var i = 0; i < pairs.length; ++i) {
24626                var pair = pairs[i].split('=');
24627                // if the key does not exist, create it
24628                if (!result[pair[0]]) {
24629                    result[pair[0]] = pair[1];
24630                } else {
24631                    // make it an array
24632                    if (!(result[pair[0]] instanceof Array)) {
24633                        result[pair[0]] = [result[pair[0]]];
24634                    }
24635                    result[pair[0]].push(pair[1]);
24636                }
24637            }
24638        }
24639        return result;
24640    };
24641
24642    /**
24643     * Replace flags in a input string. Flags are keywords surrounded with curly
24644     * braces.
24645     * @param {String} inputStr The input string.
24646     * @param {Object} values A object of {value, unit}.
24647     * @example
24648     *    var values = {"length": { "value": 33, "unit": "cm" } };
24649     *    var str = "The length is: {length}.";
24650     *    var res = dwv.utils.replaceFlags(str, values); // "The length is: 33 cm."
24651     * @return {String} The result string.
24652     */
24653    dwv.utils.replaceFlags = function(inputStr, values) {
24654        var res = "";
24655        // check input string
24656        if (inputStr === null || typeof inputStr === "undefined") {
24657            return res;
24658        }
24659        res = inputStr;
24660        // check values
24661        if (values === null || typeof values === "undefined") {
24662            return res;
24663        }
24664        // loop through values keys
24665        var keys = Object.keys(values);
24666        for (var i = 0; i < keys.length; ++i) {
24667            var valueObj = values[keys[i]];
24668            if (valueObj !== null && typeof valueObj !== "undefined" &&
24669                valueObj.value !== null && typeof valueObj.value !== "undefined") {
24670                // value string
24671                var valueStr = valueObj.value.toPrecision(4);
24672                // add unit if available
24673                // space or no space? Yes apart from degree...
24674                // check: https://en.wikipedia.org/wiki/Space_(punctuation)#Spaces_and_unit_symbols
24675                if (valueObj.unit !== null && typeof valueObj.unit !== "undefined" &&
24676                    valueObj.unit.length !== 0) {
24677                    if (valueObj.unit !== "degree") {
24678                        valueStr += " ";
24679                    }
24680                    valueStr += valueObj.unit;
24681                }
24682                // flag to replace
24683                var flag = '{' + keys[i] + '}';
24684                // replace
24685                res = res.replace(flag, valueStr);
24686            }
24687        }
24688        // return
24689        return res;
24690    };
24691
24692    /**
24693     * Replace flags in a input string. Flags are keywords surrounded with curly
24694     * braces.
24695     * @param {String} inputStr The input string.
24696     * @param {Array}
24696 values An array of strings.
24697     * @example
24698     *    var values = ["a", "b"];
24699     *    var str = "The length is: {v0}. The size is: {v1}";
24700     *    var res = dwv.utils.replaceFlags2(str, values); // "The length is: a. The size is: b"
24701     * @return {String} The result string.
24702     */
24703    dwv.utils.replaceFlags2 = function(inputStr, values) {
24704        var res = inputStr;
24705        for (var j = 0; j < values.length; ++j) {
24706            res = res.replace("{v" + j + "}", values[j]);
24707        }
24708        return res;
24709    };
24710
24711    dwv.utils.createDefaultReplaceFormat = function(values) {
24712        var res = "";
24713        for (var j = 0; j < values.length; ++j) {
24714            if (j !== 0) {
24715                res += ", ";
24716            }
24717            res += "{v" + j + "}";
24718        }
24719        return res;
24720    };
24721
24722    // namespaces
24723    var dwv = dwv || {};
24724    dwv.utils = dwv.utils || {};
24725
24726    /**
24727     * Thread Pool.
24728     * Highly inspired from {@link http://www.smartjava.org/content/html5-easily-parallelize-jobs-using-web-workers-and-threadpool}.
24729     * @constructor
24730     * @param {Number} poolSize The size of the pool.
24731     */
24732    dwv.utils.ThreadPool = function(poolSize) {
24733
24734        // task queue
24735        var taskQueue = [];
24736        // lsit of available threads
24737        var freeThreads = [];
24738        // list of running threads (unsed in abort)
24739        var runningThreads = [];
24740
24741        /**
24742         * Initialise.
24743         */
24744        this.init = function() {
24745            // create 'poolSize' number of worker threads
24746            for (var i = 0; i < poolSize; ++i) {
24747                freeThreads.push(new dwv.utils.WorkerThread(this));
24748            }
24749        };
24750
24751        /**
24752         * Add a worker task to the queue.
24753         * Will be run when a thread is made available.
24754         * @return {Object} workerTask The task to add.
24755         */
24756        this.addWorkerTask = function(workerTask) {
24757            if (freeThreads.length > 0) {
24758                // get the first free worker thread
24759                var workerThread = freeThreads.shift();
24760                // run the input task
24761                workerThread.run(workerTask);
24762                // add the thread to the runnning list
24763                runningThreads.push(workerThread);
24764            } else {
24765                // no free thread, add task to queue
24766                taskQueue.push(workerTask);
24767            }
24768        };
24769
24770        /**
24771         * Abort all threads.
24772         */
24773        this.abort = function() {
24774            // clear tasks
24775            taskQueue = [];
24776            // cancel running workers
24777            for (var i = 0; i < runningThreads.length; ++i) {
24778                runningThreads[i].stop();
24779            }
24780            runningThreads = [];
24781            // re-init
24782            this.init();
24783        };
24784
24785        /**
24786         * Free a worker thread.
24787         * @param {Object} workerThread The thread to free.
24788         */
24789        this.freeWorkerThread = function(workerThread) {
24790            // send worker end
24791            this.onworkerend();
24792
24793            if (taskQueue.length > 0) {
24794                // get waiting task
24795                var workerTask = taskQueue.shift();
24796                // use input thread to run the waiting task
24797                workerThread.run(workerTask);
24798            } else {
24799                // no task to run, add to free list
24800                freeThreads.push(workerThread);
24801                // remove from running list
24802                for (var i = 0; i < runningThreads.length; ++i) {
24803                    if (runningThreads[i].getId() === workerThread.getId()) {
24804                        runningThreads.splice(i, 1);
24805                    }
24806                }
24807                // the work is done when the queue is back to its initial size
24808                if (freeThreads.length === poolSize) {
24809                    this.onpoolworkend();
24810                }
24811            }
24812        };
24813    };
24814
24815    /**
24816     * Handle a pool work end event.
24817     * Default does nothing.
24818     */
24819    dwv.utils.ThreadPool.prototype.onpoolworkend = function() {};
24820    /**
24821     * Handle a pool worker end event.
24822     * Default does nothing.
24823     */
24824    dwv.utils.ThreadPool.prototype.onworkerend = function() {};
24825
24826    /**
24827     * Worker thread.
24828     * @external Worker
24829     * @constructor
24830     * @param {Object} parentPool The parent pool.
24831     */
24832    dwv.utils.WorkerThread = function(parentPool) {
24833        // closure to self
24834        var self = this;
24835
24836        // thread ID
24837        var id = Math.random().toString(36).substring(2, 15);
24838
24839        // running task
24840        var runningTask = {};
24841        // worker used to run task
24842        var worker;
24843
24844        /**
24845         * Get the thread ID.
24846         * @return {String} The thread ID (alphanumeric).
24847         */
24848        this.getId = function() {
24849            return id;
24850        };
24851
24852        /**
24853         * Run a worker task
24854         * @param {Object} workerTask The task to run.
24855         */
24856        this.run = function(workerTask) {
24857            // store task
24858            runningTask = workerTask;
24859            // create a new web worker
24860            if (runningTask.script !== null) {
24861                worker = new Worker(runningTask.script);
24862                worker.addEventListener('message', ontaskend, false);
24863                // launch the worker
24864                worker.postMessage(runningTask.startMessage);
24865            }
24866        };
24867
24868        /**
24869         * Stop a run and free the thread.
24870         */
24871        this.stop = function() {
24872            // stop the worker
24873            worker.terminate();
24874            // tell the parent pool this thread is free
24875            parentPool.freeWorkerThread(this);
24876        };
24877
24878        /**
24879         * Handle once the task is done.
24880         * For now assume we only get a single callback from a worker
24881         * which also indicates the end of this worker.
24882         * @param {Object} event The callback event.
24883         */
24884        function ontaskend(event) {
24885            // pass to original callback
24886            runningTask.callback(event);
24887            // stop the worker and free the thread
24888            self.stop();
24889        }
24890
24891    };
24892
24893    /**
24894     * Worker task.
24895     * @constructor
24896     * @param {String} script The worker script.
24897     * @param {Function} callback The worker callback.
24898     * @param {Object} message The data to pass to the worker.
24899     */
24900    dwv.utils.WorkerTask = function(script, callback, message) {
24901        // worker script
24902        this.script = script;
24903        // worker callback
24904        this.callback = callback;
24905        // worker start message
24906        this.startMessage = message;
24907    };
24908
24909    // namespaces
24910    var dwv = dwv || {};
24911    dwv.utils = dwv.utils || {};
24912    /** @namespace */
24913    dwv.utils.base = dwv.utils.base || {};
24914
24915    /**
24916     * Split an input URI:
24917     *  'root?key0=val00&key0=val01&key1=val10' returns
24918     *  { base : root, query : [ key0 : [val00, val01], key1 : val1 ] }
24919     * Returns an empty object if the input string is not correct (null, empty...)
24920     *  or if it is not a query string (no question mark).
24921     * @param {String} inputStr The string to split.
24922     * @return {Object} The split string.
24923     */
24924    dwv.utils.splitUri = function(uri) {
24925        // result
24926        var result = {};
24927        // check if query string
24928        var sepIndex = null;
24929        if (uri && (sepIndex = uri.indexOf('?')) !== -1) {
24930            // base: before the '?'
24931            result.base = uri.substr(0, sepIndex);
24932            // query : after the '?' and until possible '#'
24933            var hashIndex = uri.indexOf('#');
24934            if (hashIndex === -1) {
24935                hashIndex = uri.length;
24936            }
24937            var query = uri.substr(sepIndex + 1, (hashIndex - 1 - sepIndex));
24938            // split key/value pairs of the query
24939            result.query = dwv.utils.splitKeyValueString(query);
24940        }
24941        // return
24942        return result;
24943    };
24944
24945    /**
24946     * Get the query part, split into an array, of an input URI.
24947     * The URI scheme is: 'base?query#fragment'
24948     * @param {String } uri The input URI.
24949     * @return {Object} The query part, split into an array, of the input URI.
24950     */
24951    dwv.utils.getUriQuery = function(uri) {
24952        // split
24953        var parts = dwv.utils.splitUri(uri);
24954        // check not empty
24955        if (Object.keys(parts).length === 0) {
24956            return null;
24957        }
24958        // return query
24959        return parts.query;
24960    };
24961
24962    /**
24963     * Generic URI query decoder.
24964     * Supports manifest:
24965     *   [dwv root]?input=encodeURIComponent('[manifest file]')&type=manifest
24966     * or encoded URI with base and key value/pairs:
24967     *   [dwv root]?input=encodeURIComponent([root]?key0=value0&key1=value1)
24968     *  @param {String} query The query part to the input URI.
24969     *  @param {Function} callback The function to call with the decoded file urls.
24970     */
24971    dwv.utils.base.decodeQuery = function(query, callback) {
24972        // manifest
24973        if (query.type && query.type === "manifest") {
24974            dwv.utils.decodeManifestQuery(query, callback);
24975        }
24976        // default case: encoded URI with base and key/value pairs
24977        else {
24978            callback(dwv.utils.decodeKeyValueUri(query.input, query.dwvReplaceMode));
24979        }
24980    };
24981
24982    /**
24983     * Decode a Key/Value pair URI. If a key is repeated, the result
24984     * be an array of base + each key.
24985     * @param {String} uri The URI to decode.
24986     * @param {String} replaceMode The key replace more.
24987     *   replaceMode can be:
24988     *   - key (default): keep the key
24989     *   - other than key: do not use the key
24990     *   'file' is a special case where the '?' of the query is not kept.
24991     * @return The list of input file urls.
24992     */
24993    dwv.utils.decodeKeyValueUri = function(uri, replaceMode) {
24994        var result = [];
24995
24996        // repeat key replace mode (default to keep key)
24997        var repeatKeyReplaceMode = "key";
24998        if (replaceMode) {
24999            repeatKeyReplaceMode = replaceMode;
25000        }
25001
25002        // decode input URI
25003        var queryUri = decodeURIComponent(uri);
25004        // get key/value pairs from input URI
25005        var inputQueryPairs = dwv.utils.splitUri(queryUri);
25006        if (Object.keys(inputQueryPairs).length === 0) {
25007            result.push(queryUri);
25008        } else {
25009            var keys = Object.keys(inputQueryPairs.query);
25010            // find repeat key
25011            var repeatKey = null;
25012            for (var i = 0; i < keys.length; ++i) {
25013                if (inputQueryPairs.query[keys[i]] instanceof Array) {
25014                    repeatKey = keys[i];
25015                    break;
25016                }
25017            }
25018
25019            if (!repeatKey) {
25020                result.push(queryUri);
25021            } else {
25022                var repeatList = inputQueryPairs.query[repeatKey];
25023                // build base uri
25024                var baseUrl = inputQueryPairs.base;
25025                // do not add '?' when the repeatKey is 'file'
25026                // root/path/to/?file=0.jpg&file=1.jpg
25027                if (repeatKey !== "file") {
25028                    baseUrl += "?";
25029                }
25030                var gotOneArg = false;
25031                for (var j = 0; j < keys.length; ++j) {
25032                    if (keys[j] !== repeatKey) {
25033                        if (gotOneArg) {
25034                            baseUrl += "&";
25035                        }
25036                        baseUrl += keys[j] + "=" + inputQueryPairs.query[keys[j]];
25037                        gotOneArg = true;
25038                    }
25039                }
25040                // append built urls to result
25041                var url;
25042                for (var k = 0; k < repeatList.length; ++k) {
25043                    url = baseUrl;
25044                    if (gotOneArg) {
25045                        url += "&";
25046                    }
25047                    if (repeatKeyReplaceMode === "key") {
25048                        url += repeatKey + "=";
25049                    }
25050                    // other than 'key' mode: do nothing
25051                    url += repeatList[k];
25052                    result.push(url);
25053                }
25054            }
25055        }
25056        // return
25057        return result;
25058    };
25059
25060    /**
25061     * Decode a manifest query.
25062     * @external XMLHttpRequest
25063     * @param {Object} query The manifest query: {input, nslices},
25064     *   with input the input URI and nslices the number of slices.
25065     * @param {Function} The function to call with the decoded urls.
25066     */
25067    dwv.utils.decodeManifestQuery = function(query, callback) {
25068        var uri = "";
25069        if (query.input[0] === '/') {
25070            uri = window.location.protocol + "//" + window.location.host;
25071        }
25072        // TODO: needs to be decoded (decodeURIComponent?
25073        uri += query.input;
25074
25075        // handle error
25076        var onError = function( /*event*/ ) {
25077            console.warn("RequestError while receiving manifest: " + this.status);
25078        };
25079
25080        // handle load
25081        var onLoad = function( /*event*/ ) {
25082            callback(dwv.utils.decodeManifest(this.responseXML, query.nslices));
25083        };
25084
25085        var request = new XMLHttpRequest();
25086        request.open('GET', decodeURIComponent(uri), true);
25087        request.responseType = "document";
25088        request.onload = onLoad;
25089        request.onerror = onError;
25090        request.send(null);
25091    };
25092
25093    /**
25094     * Decode an XML manifest.
25095     * @param {Object} manifest The manifest to decode.
25096     * @param {Number} nslices The number of slices to load.
25097     */
25098    dwv.utils.decodeManifest = function(manifest, nslices) {
25099        var result = [];
25100        // wado url
25101        var wadoElement = manifest.getElementsByTagName("wado_query");
25102        var wadoURL = wadoElement[0].getAttribute("wadoURL");
25103        var rootURL = wadoURL + "?requestType=WADO&contentType=application/dicom&";
25104        // patient list
25105        var patientList = manifest.getElementsByTagName("Patient");
25106        if (patientList.length > 1) {
25107            console.warn("More than one patient, loading first one.");
25108        }
25109        // study list
25110        var studyList = patientList[0].getElementsByTagName("Study");
25111        if (studyList.length > 1) {
25112            console.warn("More than one study, loading first one.");
25113        }
25114        var studyUID = studyList[0].getAttribute("StudyInstanceUID");
25115        // series list
25116        var seriesList = studyList[0].getElementsByTagName("Series");
25117        if (seriesList.length > 1) {
25118            console.warn("More than one series, loading first one.");
25119        }
25120        var seriesUID = seriesList[0].getAttribute("SeriesInstanceUID");
25121        // instance list
25122        var instanceList = seriesList[0].getElementsByTagName("Instance");
25123        // loop on instances and push links
25124        var max = instanceList.length;
25125        if (nslices < max) {
25126            max = nslices;
25127        }
25128        for (var i = 0; i < max; ++i) {
25129            var sopInstanceUID = instanceList[i].getAttribute("SOPInstanceUID");
25130            var link = rootURL +
25131                "&studyUID=" + studyUID +
25132                "&seriesUID=" + seriesUID +
25133                "&objectUID=" + sopInstanceUID;
25134            result.push(link);
25135        }
25136        // return
25137        return result;
25138    };
25139
25140    return dwv;
25141}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.