diff --git a/.eslint-full.js b/.eslint-full.js new file mode 100644 index 0000000000..40474dbba3 --- /dev/null +++ b/.eslint-full.js @@ -0,0 +1,9 @@ +module.exports = { + plugins: [ + 'jsdoc' + ], + extends: [ + '.eslintrc.js', + 'plugin:jsdoc/recommended', + ] +}; diff --git a/.eslintrc.js b/.eslintrc.js index 8988cd98b7..fd0a6245ef 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,12 +8,8 @@ module.exports = { globals: { dwv: 'readonly' }, - plugins: [ - 'jsdoc' - ], extends: [ 'eslint:recommended', - 'plugin:jsdoc/recommended' ], rules: { // require triple equal diff --git a/.github/workflows/nodejs-ci.yml b/.github/workflows/nodejs-ci.yml index 3e1ebfcb4b..e4a8e126dd 100644 --- a/.github/workflows/nodejs-ci.yml +++ b/.github/workflows/nodejs-ci.yml @@ -21,11 +21,6 @@ jobs: - name: Run lint run: yarn run lint - name: Run tests - run: yarn run test-ci --coverage --verbose + run: yarn run test-ci - name: Build run: yarn run build - - name: Coveralls - uses: coverallsapp/github-action@master - with: - path-to-lcov: 'build/coverage/dwv/report-lcovonly.txt' - github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Gruntfile.js b/Gruntfile.js index e448be8fb4..daf80275a3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,6 +11,9 @@ module.exports = function (grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), eslint: { + options: { + overrideConfigFile: '.eslint-full.js' + }, files: [ 'Gruntfile.js', 'karma.conf.js', diff --git a/changelog.md b/changelog.md index 4e4ece6471..2ecc287dcb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,28 @@ # Changelog +## [v0.30.0](https://github.com/ivmartel/dwv/releases/tag/v0.30.0) - 02/12/2021 + +### Added + +- Allow for manual frame add [#1034](https://github.com/ivmartel/dwv/issues/1034) +- Add support for multi-resolution data [#1003](https://github.com/ivmartel/dwv/issues/1003) +- Remove coveralls and use karma coverage check [#1002](https://github.com/ivmartel/dwv/issues/1002) +- Use the same (web) worker for multiple data decode [#993](https://github.com/ivmartel/dwv/issues/993) +- Add text encoding in data writer [#963](https://github.com/ivmartel/dwv/issues/963) +- Support anisotropic pixels [#952](https://github.com/ivmartel/dwv/issues/952) +- Check data allocation size to avoid error [#951](https://github.com/ivmartel/dwv/issues/951) +- Allow to load files based on UID [#949](https://github.com/ivmartel/dwv/issues/949) +- Simplify image class data storage [#905](https://github.com/ivmartel/dwv/issues/905) +- Multi-slice view [#133](https://github.com/ivmartel/dwv/issues/133) +- Add MPR [#4](https://github.com/ivmartel/dwv/issues/4) + +### Fixed + +- Check var length before accessing it [#1017](https://github.com/ivmartel/dwv/issues/1017) +- Drag&drop of the ruler label causes console errors [#994](https://github.com/ivmartel/dwv/issues/994) + +--- + ## [v0.29.1](https://github.com/ivmartel/dwv/releases/tag/v0.29.1) - 11/06/2021 ### Added diff --git a/decoders/dwv/decode-rle.js b/decoders/dwv/decode-rle.js index 995abf78da..42da2cf755 100644 --- a/decoders/dwv/decode-rle.js +++ b/decoders/dwv/decode-rle.js @@ -2,21 +2,21 @@ * RLE decoder worker. */ // Do not warn if these variables were not defined before. -/* global importScripts, self, JpxImage */ +/* global importScripts */ importScripts('rle.js'); self.addEventListener('message', function (event) { - // decode DICOM buffer - var decoder = new dwv.decoder.RleDecoder(); - // post decoded data - self.postMessage([decoder.decode( - event.data.buffer, - event.data.meta.bitsAllocated, - event.data.meta.isSigned, - event.data.meta.sliceSize, - event.data.meta.samplesPerPixel, - event.data.meta.planarConfiguration )]); + // decode DICOM buffer + var decoder = new dwv.decoder.RleDecoder(); + // post decoded data + self.postMessage([decoder.decode( + event.data.buffer, + event.data.meta.bitsAllocated, + event.data.meta.isSigned, + event.data.meta.sliceSize, + event.data.meta.samplesPerPixel, + event.data.meta.planarConfiguration)]); }, false); diff --git a/decoders/dwv/rle.js b/decoders/dwv/rle.js index 975c29a0fd..de1c686b06 100644 --- a/decoders/dwv/rle.js +++ b/decoders/dwv/rle.js @@ -20,110 +20,110 @@ dwv.decoder.RleDecoder = function () {}; * @returns The decoded buffer. * @see http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_G.3.html */ -dwv.decoder.RleDecoder.prototype.decode = function ( buffer, - bitsAllocated, isSigned, sliceSize, samplesPerPixel, planarConfiguration ) { +dwv.decoder.RleDecoder.prototype.decode = function (buffer, + bitsAllocated, isSigned, sliceSize, samplesPerPixel, planarConfiguration) { - // bytes per element - var bpe = bitsAllocated / 8; + // bytes per element + var bpe = bitsAllocated / 8; - // input - var inputDataView = new DataView(buffer.buffer, buffer.byteOffset); - var inputArray = new Int8Array(buffer.buffer, buffer.byteOffset); - // output - var outputBuffer = new ArrayBuffer(sliceSize * samplesPerPixel * bpe); - var outputArray = new Int8Array(outputBuffer); + // input + var inputDataView = new DataView(buffer.buffer, buffer.byteOffset); + var inputArray = new Int8Array(buffer.buffer, buffer.byteOffset); + // output + var outputBuffer = new ArrayBuffer(sliceSize * samplesPerPixel * bpe); + var outputArray = new Int8Array(outputBuffer); - // first value of the RLE header is the number of segments - var numberOfSegments = inputDataView.getInt32(0, true); + // first value of the RLE header is the number of segments + var numberOfSegments = inputDataView.getInt32(0, true); - // index increment in output array - var outputIndexIncrement = 1; - var incrementFactor = 1; - if (samplesPerPixel !== 1 && planarConfiguration === 0) { - incrementFactor *= samplesPerPixel; - } - if (bpe !== 1 ) { - incrementFactor *= bpe; - } - outputIndexIncrement *= incrementFactor; + // index increment in output array + var outputIndexIncrement = 1; + var incrementFactor = 1; + if (samplesPerPixel !== 1 && planarConfiguration === 0) { + incrementFactor *= samplesPerPixel; + } + if (bpe !== 1) { + incrementFactor *= bpe; + } + outputIndexIncrement *= incrementFactor; - // loop on segments - var outputIndex = 0; - var inputIndex = 0; - var remainder = 0; - var maxOutputIndex = 0; - var groupOutputIndex = 0; - for (var segment = 0; segment < numberOfSegments; ++segment) { - // handle special cases: - // - more than one sample per pixel: one segment per channel - // - 16bits: sort high and low bytes - if (incrementFactor !== 1) { - remainder = segment % incrementFactor; - if (remainder === 0) { - groupOutputIndex = maxOutputIndex; - } - outputIndex = groupOutputIndex + remainder; - // 16bits data - if (bpe === 2) { - outputIndex += (remainder % bpe ? -1 : 1); - } - } + // loop on segments + var outputIndex = 0; + var inputIndex = 0; + var remainder = 0; + var maxOutputIndex = 0; + var groupOutputIndex = 0; + for (var segment = 0; segment < numberOfSegments; ++segment) { + // handle special cases: + // - more than one sample per pixel: one segment per channel + // - 16bits: sort high and low bytes + if (incrementFactor !== 1) { + remainder = segment % incrementFactor; + if (remainder === 0) { + groupOutputIndex = maxOutputIndex; + } + outputIndex = groupOutputIndex + remainder; + // 16bits data + if (bpe === 2) { + outputIndex += (remainder % bpe ? -1 : 1); + } + } - // RLE header: list of segment sizes - var segmentStartIndex = inputDataView.getInt32((segment + 1) * 4, true); - var nextSegmentStartIndex = inputDataView.getInt32((segment + 2) * 4, true); - if (segment === numberOfSegments - 1 || nextSegmentStartIndex === 0) { - nextSegmentStartIndex = buffer.length; + // RLE header: list of segment sizes + var segmentStartIndex = inputDataView.getInt32((segment + 1) * 4, true); + var nextSegmentStartIndex = inputDataView.getInt32((segment + 2) * 4, true); + if (segment === numberOfSegments - 1 || nextSegmentStartIndex === 0) { + nextSegmentStartIndex = buffer.length; + } + // decode segment + inputIndex = segmentStartIndex; + var count = 0; + while (inputIndex < nextSegmentStartIndex) { + // get the count value + count = inputArray[inputIndex]; + ++inputIndex; + // store according to count + if (count >= 0 && count <= 127) { + // output the next count+1 bytes literally + for (var i = 0; i < count + 1; ++i) { + // store + outputArray[outputIndex] = inputArray[inputIndex]; + // increment indexes + ++inputIndex; + outputIndex += outputIndexIncrement; } - // decode segment - inputIndex = segmentStartIndex; - var count = 0; - while (inputIndex < nextSegmentStartIndex) { - // get the count value - count = inputArray[inputIndex]; - ++inputIndex; - // store according to count - if (count >= 0 && count <= 127) { - // output the next count+1 bytes literally - for (var i = 0; i < count + 1; ++i) { - // store - outputArray[outputIndex] = inputArray[inputIndex]; - // increment indexes - ++inputIndex; - outputIndex += outputIndexIncrement; - } - } else if (count <= -1 && count >= -127) { - // output the next byte -count+1 times - var value = inputArray[inputIndex]; - ++inputIndex; - for (var j = 0; j < -count + 1; ++j) { - // store - outputArray[outputIndex] = value; - // increment index - outputIndex += outputIndexIncrement; - } - } + } else if (count <= -1 && count >= -127) { + // output the next byte -count+1 times + var value = inputArray[inputIndex]; + ++inputIndex; + for (var j = 0; j < -count + 1; ++j) { + // store + outputArray[outputIndex] = value; + // increment index + outputIndex += outputIndexIncrement; } + } + } - if (outputIndex > maxOutputIndex) { - maxOutputIndex = outputIndex; - } + if (outputIndex > maxOutputIndex) { + maxOutputIndex = outputIndex; } + } - var decodedBuffer = null; - if (bitsAllocated === 8) { - if (isSigned) { - decodedBuffer = new Int8Array(outputBuffer); - } else { - decodedBuffer = new Uint8Array(outputBuffer); - } - } else if (bitsAllocated === 16) { - if (isSigned) { - decodedBuffer = new Int16Array(outputBuffer); - } else { - decodedBuffer = new Uint16Array(outputBuffer); - } + var decodedBuffer = null; + if (bitsAllocated === 8) { + if (isSigned) { + decodedBuffer = new Int8Array(outputBuffer); + } else { + decodedBuffer = new Uint8Array(outputBuffer); + } + } else if (bitsAllocated === 16) { + if (isSigned) { + decodedBuffer = new Int16Array(outputBuffer); + } else { + decodedBuffer = new Uint16Array(outputBuffer); } + } - return decodedBuffer; + return decodedBuffer; }; diff --git a/decoders/pdfjs/decode-jpeg2000.js b/decoders/pdfjs/decode-jpeg2000.js index e83b75f2c5..12f1e326a3 100644 --- a/decoders/pdfjs/decode-jpeg2000.js +++ b/decoders/pdfjs/decode-jpeg2000.js @@ -2,17 +2,17 @@ * JPEG 2000 decoder worker. */ // Do not warn if these variables were not defined before. -/* global importScripts, self, JpxImage */ +/* global importScripts, JpxImage */ -importScripts('jpx.js', 'util.js', 'arithmetic_decoder.js'); +importScripts('jpx.js', 'util.js', 'arithmetic_decoder.js'); self.addEventListener('message', function (event) { - - // decode DICOM buffer - var decoder = new JpxImage(); - decoder.parse( event.data.buffer ); - // post decoded data - var res = decoder.tiles[0].items; - self.postMessage([res]); - + + // decode DICOM buffer + var decoder = new JpxImage(); + decoder.parse(event.data.buffer); + // post decoded data + var res = decoder.tiles[0].items; + self.postMessage([res]); + }, false); diff --git a/decoders/pdfjs/decode-jpegbaseline.js b/decoders/pdfjs/decode-jpegbaseline.js index 1967bab892..38f5f89811 100644 --- a/decoders/pdfjs/decode-jpegbaseline.js +++ b/decoders/pdfjs/decode-jpegbaseline.js @@ -2,17 +2,17 @@ * JPEG Baseline decoder worker. */ // Do not warn if these variables were not defined before. -/* global importScripts, self, JpegImage */ +/* global importScripts, JpegImage */ -importScripts('jpg.js'); +importScripts('jpg.js'); self.addEventListener('message', function (event) { - - // decode DICOM buffer - var decoder = new JpegImage(); - decoder.parse( event.data.buffer ); - // post decoded data - var res = decoder.getData(decoder.width,decoder.height); - self.postMessage([res]); - + + // decode DICOM buffer + var decoder = new JpegImage(); + decoder.parse(event.data.buffer); + // post decoded data + var res = decoder.getData(decoder.width, decoder.height); + self.postMessage([res]); + }, false); diff --git a/decoders/rii-mango/decode-jpegloss.js b/decoders/rii-mango/decode-jpegloss.js index 658c9a5ba2..1ee6c5ad9c 100644 --- a/decoders/rii-mango/decode-jpegloss.js +++ b/decoders/rii-mango/decode-jpegloss.js @@ -2,33 +2,33 @@ * JPEG Lossless decoder worker. */ // Do not warn if these variables were not defined before. -/* global importScripts, self, jpeg */ +/* global importScripts, jpeg */ importScripts('lossless-min.js'); self.addEventListener('message', function (event) { - // bytes per element - var bpe = event.data.meta.bitsAllocated / 8; - // decode DICOM buffer - var buf = new Uint8Array(event.data.buffer); - var decoder = new jpeg.lossless.Decoder(); - var decoded = decoder.decode(buf.buffer, 0, buf.buffer.byteLength, bpe); - // post decoded data - var res = null; - if (event.data.meta.bitsAllocated === 8) { - if (event.data.meta.isSigned) { - res = new Int8Array(decoded.buffer); - } else { - res = new Uint8Array(decoded.buffer); - } - } else if (event.data.meta.bitsAllocated === 16) { - if (event.data.meta.isSigned) { - res = new Int16Array(decoded.buffer); - } else { - res = new Uint16Array(decoded.buffer); - } + // bytes per element + var bpe = event.data.meta.bitsAllocated / 8; + // decode DICOM buffer + var buf = new Uint8Array(event.data.buffer); + var decoder = new jpeg.lossless.Decoder(); + var decoded = decoder.decode(buf.buffer, 0, buf.buffer.byteLength, bpe); + // post decoded data + var res = null; + if (event.data.meta.bitsAllocated === 8) { + if (event.data.meta.isSigned) { + res = new Int8Array(decoded.buffer); + } else { + res = new Uint8Array(decoded.buffer); } - self.postMessage([res]); + } else if (event.data.meta.bitsAllocated === 16) { + if (event.data.meta.isSigned) { + res = new Int16Array(decoded.buffer); + } else { + res = new Uint16Array(decoded.buffer); + } + } + self.postMessage([res]); }, false); diff --git a/dist/dwv.js b/dist/dwv.js index dd3deba4c8..38e644a383 100644 --- a/dist/dwv.js +++ b/dist/dwv.js @@ -1,4 +1,4 @@ -/*! dwv 0.29.1 2021-06-11 17:43:54 */ +/*! dwv 0.30.0 2021-12-02 15:55:09 */ // Inspired from umdjs // See https://github.com/umdjs/umd/blob/master/templates/returnExports.js (function (root, factory) { @@ -82,14 +82,11 @@ dwv.App = function () { // toolbox controller var toolboxController = null; - // layer controller - var layerController = null; - // load controller var loadController = null; - // first load item flag - var isFirstLoadItem = null; + // stage + var stage = null; // UndoStack var undoStack = null; @@ -108,54 +105,66 @@ dwv.App = function () { /** * Get the image. * + * @param {number} index The data index. * @returns {Image} The associated image. */ - this.getImage = function () { - return dataController.get(0).image; + this.getImage = function (index) { + return dataController.get(index).image; + }; + /** + * Get the last loaded image. + * + * @returns {Image} The image. + */ + this.getLastImage = function () { + return dataController.get(dataController.length() - 1).image; }; /** * Set the image. * + * @param {number} index The data index. * @param {Image} img The associated image. */ - this.setImage = function (img) { - dataController.setImage(img, 0); + this.setImage = function (index, img) { + dataController.setImage(index, img); }; - /** - * Get the meta data. + * Set the last image. * - * @returns {object} The list of meta data. + * @param {Image} img The associated image. */ - this.getMetaData = function () { - return dataController.get(0).meta; + this.setLastImage = function (img) { + dataController.setImage(dataController.length() - 1, img); }; /** - * Is the data mono-slice? + * Get the meta data. * - * @returns {boolean} True if the data only contains one slice. + * @param {number} index The data index. + * @returns {object} The list of meta data. */ - this.isMonoSliceData = function () { - return loadController.isMonoSliceData(); + this.getMetaData = function (index) { + return dataController.get(index).meta; }; + /** - * Is the data mono-frame? + * Get the number of loaded data. * - * @returns {boolean} True if the data only contains one frame. + * @returns {number} The number. */ - this.isMonoFrameData = function () { - var viewLayer = layerController.getActiveViewLayer(); - var controller = viewLayer.getViewController(); - return controller.isMonoFrameData(); + this.getNumberOfLoadedData = function () { + return dataController.length(); }; + /** * Can the data be scrolled? * - * @returns {boolean} True if the data has more than one slice or frame. + * @returns {boolean} True if the data has a third dimension greater than one. */ this.canScroll = function () { - return !this.isMonoSliceData() || !this.isMonoFrameData(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); + var controller = viewLayer.getViewController(); + return controller.canScroll(); }; /** @@ -164,7 +173,7 @@ dwv.App = function () { * @returns {boolean} True if the data is monochrome. */ this.canWindowLevel = function () { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); var controller = viewLayer.getViewController(); return controller.canWindowLevel(); }; @@ -175,7 +184,7 @@ dwv.App = function () { * @returns {object} The scale as {x,y}. */ this.getAddedScale = function () { - return layerController.getAddedScale(); + return stage.getActiveLayerGroup().getAddedScale(); }; /** @@ -184,7 +193,7 @@ dwv.App = function () { * @returns {object} The scale as {x,y}. */ this.getBaseScale = function () { - return layerController.getBaseScale(); + return stage.getActiveLayerGroup().getBaseScale(); }; /** @@ -193,7 +202,7 @@ dwv.App = function () { * @returns {object} The offset. */ this.getOffset = function () { - return layerController.getOffset(); + return stage.getActiveLayerGroup().getOffset(); }; /** @@ -206,13 +215,44 @@ dwv.App = function () { }; /** - * Get the layer controller. - * The controller is available after the first loaded item. + * Get the active layer group. + * The layer is available after the first loaded item. * - * @returns {object} The controller. + * @returns {dwv.gui.LayerGroup} The layer group. + */ + this.getActiveLayerGroup = function () { + return stage.getActiveLayerGroup(); + }; + + /** + * Get the view layers associated to a data index. + * The layer are available after the first loaded item. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getViewLayersByDataIndex = function (index) { + return stage.getViewLayersByDataIndex(index); + }; + + /** + * Get a layer group by id. + * The layer is available after the first loaded item. + * + * @param {number} groupId The group id. + * @returns {dwv.gui.LayerGroup} The layer group. */ - this.getLayerController = function () { - return layerController; + this.getLayerGroupById = function (groupId) { + return stage.getLayerGroup(groupId); + }; + + /** + * Get the number of layer groups. + * + * @returns {number} The number of groups. + */ + this.getNumberOfLayerGroups = function () { + return stage.getNumberOfLayerGroups(); }; /** @@ -239,21 +279,27 @@ dwv.App = function () { /** * Initialise the application. * - * @param {object} opt The application options. + * @param {object} opt The application option with: + * - `dataViewConfigs`: data indexed object containing the data view + * configurations in the form of a list of objects containing: + * - divId: the HTML div id + * - orientation: optional 'axial', 'coronal' or 'sagittal' otientation + * string (default undefined keeps the original slice order) + * - `binders`: array of layerGroup binders + * - `tools`: tool name indexed object containing individual tool + * configurations + * - `viewOnFirstLoadItem`: boolean flag to trigger the first data render + * after the first loaded data or not + * - `defaultCharacterSet`: the default chraracter set string used for DICOM + * parsing */ this.init = function (opt) { // store options = opt; // defaults - if (typeof options.containerDivId === 'undefined') { - options.containerDivId = 'dwv'; - } if (typeof options.viewOnFirstLoadItem === 'undefined') { options.viewOnFirstLoadItem = true; } - if (typeof options.nSimultaneousData === 'undefined') { - options.nSimultaneousData = 1; - } // undo stack undoStack = new dwv.tool.UndoStack(); @@ -317,11 +363,11 @@ dwv.App = function () { } } // add tools to the controller - toolboxController = new dwv.ToolboxController(toolList); + toolboxController = new dwv.ctrl.ToolboxController(toolList); } // create load controller - loadController = new dwv.LoadController(options.defaultCharacterSet); + loadController = new dwv.ctrl.LoadController(options.defaultCharacterSet); loadController.onloadstart = onloadstart; loadController.onprogress = onprogress; loadController.onloaditem = onloaditem; @@ -331,27 +377,23 @@ dwv.App = function () { loadController.onabort = onabort; // create data controller - dataController = new dwv.DataController(); - }; - - /** - * Get the size available for the layer container div. - * - * @returns {object} The available width and height: {width:X; height:Y}. - */ - this.getLayerContainerSize = function () { - var size = layerController.getLayerContainerSize(); - return {width: size.x, height: size.y}; + dataController = new dwv.ctrl.DataController(); + // create stage + stage = new dwv.gui.Stage(); + if (typeof options.binders !== 'undefined') { + stage.setBinders(options.binders); + } }; /** * Get a HTML element associated to the application. * - * @param {string} name The name or id to find. + * @param {string} _name The name or id to find. * @returns {object} The found element or null. + * @deprecated */ - this.getElement = function (name) { - return dwv.gui.getElement(options.containerDivId, name); + this.getElement = function (_name) { + return null; }; /** @@ -360,7 +402,7 @@ dwv.App = function () { this.reset = function () { // clear objects dataController.reset(); - layerController.empty(); + stage.empty(); // reset undo/redo if (undoStack) { undoStack = new dwv.tool.UndoStack(); @@ -374,8 +416,8 @@ dwv.App = function () { * Reset the layout of the application. */ this.resetLayout = function () { - layerController.reset(); - layerController.draw(); + stage.reset(); + stage.draw(); }; /** @@ -406,6 +448,8 @@ dwv.App = function () { * Load a list of files. Can be image files or a state file. * * @param {Array} files The list of files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information * @fires dwv.App#loadstart * @fires dwv.App#loadprogress * @fires dwv.App#loaditem @@ -413,8 +457,12 @@ dwv.App = function () { * @fires dwv.App#error * @fires dwv.App#abort */ - this.loadFiles = function (files) { - loadController.loadFiles(files); + this.loadFiles = function (files, options) { + if (files.length === 0) { + dwv.logger.warn('Ignoring empty input file list.'); + return; + } + loadController.loadFiles(files, options); }; /** @@ -433,6 +481,10 @@ dwv.App = function () { * @fires dwv.App#abort */ this.loadURLs = function (urls, options) { + if (urls.length === 0) { + dwv.logger.warn('Ignoring empty input url list.'); + return; + } loadController.loadURLs(urls, options); }; @@ -465,26 +517,112 @@ dwv.App = function () { * Fit the display to the given size. To be called once the image is loaded. */ this.fitToContainer = function () { - layerController.fitToContainer(); - layerController.draw(); - // update style - style.setBaseScale(layerController.getBaseScale()); + var layerGroup = stage.getActiveLayerGroup(); + if (layerGroup) { + layerGroup.fitToContainer(self.getLastImage().getGeometry()); + layerGroup.draw(); + // update style + //style.setBaseScale(layerGroup.getBaseScale()); + } }; /** * Init the Window/Level display */ this.initWLDisplay = function () { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); var controller = viewLayer.getViewController(); controller.initialise(); }; /** - * Render the current data. + * Get the layer group configuration from a data index. + * Defaults to div id 'layerGroup' if no association object has been set. + * + * @param {number} dataIndex The data index. + * @returns {Array} The list of associated configs. + */ + function getViewConfigs(dataIndex) { + // check options + if (options.dataViewConfigs === null || + typeof options.dataViewConfigs === 'undefined') { + throw new Error('No available data iew configuration'); + } + var configs = null; + if (typeof options.dataViewConfigs['*'] !== 'undefined') { + configs = options.dataViewConfigs['*']; + } else { + configs = options.dataViewConfigs[dataIndex]; + } + return configs; + } + + /** + * Set the data view configuration (see the init options for details). + * + * @param {object} configs The configuration list. + */ + this.setDataViewConfig = function (configs) { + // clean up + stage.empty(); + // set new + options.dataViewConfigs = configs; + // re-bind layers + stage.bindLayerGroups(); + }; + + /** + * Set the layer groups binders. + * + * @param {Array} list The binders list. */ - this.render = function () { - layerController.draw(); + this.setLayerGroupsBinders = function (list) { + stage.setBinders(list); + }; + + /** + * Render the current data. + * + * @param {number} dataIndex The data index to render. + */ + this.render = function (dataIndex) { + if (typeof dataIndex === 'undefined' || dataIndex === null) { + throw new Error('Cannot render without data index'); + } + // loop on all configs + var viewConfigs = getViewConfigs(dataIndex); + if (!viewConfigs) { + throw new Error('No view config for data: ' + dataIndex); + } + for (var i = 0; i < viewConfigs.length; ++i) { + var config = viewConfigs[i]; + // create layer group if not done yet + // warn: needs a loaded DOM + var layerGroup = + stage.getLayerGroupWithElementId(config.divId); + if (!layerGroup) { + // create new layer group + var element = document.getElementById(config.divId); + layerGroup = stage.addLayerGroup(element); + // bind events + bindLayerGroup(layerGroup); + // optional orientation + if (typeof config.orientation !== 'undefined') { + layerGroup.setTargetOrientation( + dwv.math.getMatrixFromName(config.orientation)); + } + } + // initialise or add view + if (layerGroup.getViewLayersByDataIndex(dataIndex).length === 0) { + if (layerGroup.getNumberOfLayers() === 0) { + initialiseBaseLayers(dataIndex, config.divId); + } else { + addViewLayer(dataIndex, config.divId); + } + } + // draw + layerGroup.draw(); + } }; /** @@ -495,8 +633,11 @@ dwv.App = function () { * @param {number} cy The zoom center Y coordinate. */ this.zoom = function (step, cx, cy) { - layerController.addScale(step, {x: cx, y: cy}); - layerController.draw(); + var layerGroup = stage.getActiveLayerGroup(); + var viewController = layerGroup.getActiveViewLayer().getViewController(); + var k = viewController.getCurrentScrollPosition(); + layerGroup.addScale(step, {x: cx, y: cy, z: k}); + layerGroup.draw(); }; /** @@ -506,8 +647,9 @@ dwv.App = function () { * @param {number} ty The translation along Y. */ this.translate = function (tx, ty) { - layerController.addTranslation({x: tx, y: ty}); - layerController.draw(); + var layerGroup = stage.getActiveLayerGroup(); + layerGroup.addTranslation({x: tx, y: ty}); + layerGroup.draw(); }; /** @@ -516,7 +658,7 @@ dwv.App = function () { * @param {number} alpha The opacity ([0:1] range). */ this.setOpacity = function (alpha) { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); viewLayer.setOpacity(alpha); viewLayer.draw(); }; @@ -524,11 +666,11 @@ dwv.App = function () { /** * Get the list of drawing display details. * - * @returns {object} The list of draw details including id, slice, frame... + * @returns {object} The list of draw details including id, position... */ this.getDrawDisplayDetails = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.getDrawDisplayDetails(); }; @@ -539,7 +681,7 @@ dwv.App = function () { */ this.getDrawStoreDetails = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.getDrawStoreDetails(); }; /** @@ -549,17 +691,17 @@ dwv.App = function () { * @param {Array} drawingsDetails An array of drawings details. */ this.setDrawings = function (drawings, drawingsDetails) { + var layerGroup = stage.getActiveLayerGroup(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); drawController.setDrawings( drawings, drawingsDetails, fireEvent, this.addToUndoStack); drawController.activateDrawLayer( - viewController.getCurrentPosition(), - viewController.getCurrentFrame()); + viewController.getCurrentOrientedPosition()); }; /** * Update a drawing from its details. @@ -568,7 +710,7 @@ dwv.App = function () { */ this.updateDraw = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.updateDraw(drawDetails); }; /** @@ -576,7 +718,7 @@ dwv.App = function () { */ this.deleteDraws = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.deleteDraws(fireEvent, this.addToUndoStack); }; /** @@ -587,7 +729,7 @@ dwv.App = function () { */ this.isGroupVisible = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.isGroupVisible(drawDetails); }; /** @@ -597,7 +739,7 @@ dwv.App = function () { */ this.toogleGroupVisibility = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.toogleGroupVisibility(drawDetails); }; @@ -607,7 +749,7 @@ dwv.App = function () { * @returns {object} The state of the app as a JSON object. */ this.getState = function () { - var state = new dwv.State(); + var state = new dwv.io.State(); return state.toJSON(self); }; @@ -647,10 +789,10 @@ dwv.App = function () { * Key down event handler example. * - CRTL-Z: undo * - CRTL-Y: redo - * - CRTL-ARROW_LEFT: next frame - * - CRTL-ARROW_UP: next slice - * - CRTL-ARROW_RIGHT: previous frame - * - CRTL-ARROW_DOWN: previous slice + * - CRTL-ARROW_LEFT: next element on fourth dim + * - CRTL-ARROW_UP: next element on third dim + * - CRTL-ARROW_RIGHT: previous element on fourth dim + * - CRTL-ARROW_DOWN: previous element on third dim * * @param {object} event The key down event. * @fires dwv.tool.UndoStack#undo @@ -658,20 +800,29 @@ dwv.App = function () { */ this.defaultOnKeydown = function (event) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); + var size = viewController.getImageSize(); if (event.ctrlKey) { if (event.keyCode === 37) { // crtl-arrow-left event.preventDefault(); - viewController.decrementFrameNb(); + if (size.moreThanOne(3)) { + viewController.decrementIndex(3); + } } else if (event.keyCode === 38) { // crtl-arrow-up event.preventDefault(); - viewController.incrementSliceNb(); + if (viewController.canScroll()) { + viewController.incrementScrollIndex(); + } } else if (event.keyCode === 39) { // crtl-arrow-right event.preventDefault(); - viewController.incrementFrameNb(); + if (size.moreThanOne(3)) { + viewController.incrementIndex(3); + } } else if (event.keyCode === 40) { // crtl-arrow-down event.preventDefault(); - viewController.decrementSliceNb(); + if (viewController.canScroll()) { + viewController.decrementScrollIndex(); + } } else if (event.keyCode === 89) { // crtl-y undoStack.redo(); } else if (event.keyCode === 90) { // crtl-z @@ -704,7 +855,7 @@ dwv.App = function () { */ this.setColourMap = function (colourMap) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); viewController.setColourMapFromName(colourMap); }; @@ -715,7 +866,7 @@ dwv.App = function () { */ this.setWindowLevelPreset = function (preset) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); viewController.setWindowLevelPreset(preset); }; @@ -725,24 +876,33 @@ dwv.App = function () { * @param {string} tool The tool. */ this.setTool = function (tool) { - var layer = null; - var previousLayer = null; - if (tool === 'Draw' || - tool === 'Livewire' || - tool === 'Floodfill') { - layer = layerController.getActiveDrawLayer(); - previousLayer = layerController.getActiveViewLayer(); - } else { - layer = layerController.getActiveViewLayer(); - previousLayer = layerController.getActiveDrawLayer(); - } - if (previousLayer) { - toolboxController.detachLayer(previousLayer); + // bind tool to layer: not really important which layer since + // tools are responsible for finding the event source layer + // but there needs to be at least one binding... + for (var i = 0; i < stage.getNumberOfLayerGroups(); ++i) { + var layerGroup = stage.getLayerGroup(i); + // unbind previous layer + var vl = layerGroup.getActiveViewLayer(); + if (vl) { + toolboxController.unbindLayer(vl); + } + var dl = layerGroup.getActiveDrawLayer(); + if (dl) { + toolboxController.unbindLayer(dl); + } + // bind new layer + var layer = null; + if (tool === 'Draw' || + tool === 'Livewire' || + tool === 'Floodfill') { + layer = layerGroup.getActiveDrawLayer(); + } else { + layer = layerGroup.getActiveViewLayer(); + } + toolboxController.bindLayer(layer); } - // detach to avoid possible double attach - toolboxController.detachLayer(layer); - toolboxController.attachLayer(layer); + // set toolbox tool toolboxController.setSelectedTool(tool); }; @@ -827,13 +987,6 @@ dwv.App = function () { * @private */ function onloadstart(event) { - isFirstLoadItem = true; - - if (event.loadtype === 'image' && - dataController.length() === options.nSimultaneousData) { - self.reset(); - } - /** * Load start event. * @@ -880,27 +1033,31 @@ dwv.App = function () { function onloaditem(event) { // check event if (typeof event.data === 'undefined') { - dwv.logger.error('Missing loaditem event data ' + event); + dwv.logger.error('Missing loaditem event data.'); } if (typeof event.loadtype === 'undefined') { - dwv.logger.error('Missing loaditem event load type ' + event); + dwv.logger.error('Missing loaditem event load type.'); } - // number returned by image.appendSlice - var sliceNb = null; + var isFirstLoadItem = event.isfirstitem; + var isTimepoint = typeof event.timepoint !== 'undefined'; + var timeId = 0; + if (isTimepoint) { + timeId = event.timepoint.id; + } var eventMetaData = null; if (event.loadtype === 'image') { - if (isFirstLoadItem) { + if (isFirstLoadItem && timeId === 0) { dataController.addNew(event.data.image, event.data.info); } else { - sliceNb = dataController.updateCurrent( - event.data.image, event.data.info); + dataController.update( + event.loadid, event.data.image, event.data.info, + timeId); } - eventMetaData = event.data.info; } else if (event.loadtype === 'state') { - var state = new dwv.State(); + var state = new dwv.io.State(); state.apply(self, state.fromJSON(event.data)); eventMetaData = 'state'; } @@ -923,45 +1080,11 @@ dwv.App = function () { loadtype: event.loadtype }); - // adapt context - if (event.loadtype === 'image') { - if (isFirstLoadItem) { - // create layer controller if not done yet - // warn: needs a loaded DOM - if (!layerController) { - layerController = - new dwv.LayerController(self.getElement('layerContainer')); - } - // initialise or add view - var dataIndex = dataController.getCurrentIndex(); - var data = dataController.get(dataIndex); - if (layerController.getNumberOfLayers() === 0) { - initialiseBaseLayers(data.image, data.meta, dataIndex); - } else { - addViewLayer(data.image, data.meta, dataIndex); - } - } else { - // update slice number if new slice was inserted before - var controller = - layerController.getActiveViewLayer().getViewController(); - var currentPosition = controller.getCurrentPosition(); - if (sliceNb <= currentPosition.k) { - controller.setCurrentPosition({ - i: currentPosition.i, - j: currentPosition.j, - k: currentPosition.k + 1 - }, true); - } - } - - // render if flag allows - if (isFirstLoadItem && options.viewOnFirstLoadItem) { - self.render(); - } + // render if first and flag allows + if (event.loadtype === 'image' && + isFirstLoadItem && options.viewOnFirstLoadItem) { + self.render(event.loadid); } - - // reset flag - isFirstLoadItem = false; } /** @@ -990,7 +1113,6 @@ dwv.App = function () { * @private */ function onloadend(event) { - isFirstLoadItem = null; /** * Main load end event: fired when the load finishes, * successfully or not. @@ -1051,110 +1173,154 @@ dwv.App = function () { } /** - * Bind view layer events to app. + * Bind layer group events to app. * - * @param {object} viewLayer The view layer. + * @param {object} group The layer group. * @private */ - function bindViewLayer(viewLayer) { - // propagate view events - viewLayer.propagateViewEvents(true); - for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - viewLayer.addEventListener(dwv.image.viewEventNames[j], fireEvent); - } + function bindLayerGroup(group) { + // propagate layer group events + group.addEventListener('zoomchange', fireEvent); + group.addEventListener('offsetchange', fireEvent); // propagate viewLayer events - viewLayer.addEventListener('renderstart', fireEvent); - viewLayer.addEventListener('renderend', fireEvent); - } - - /** - * Un-Bind view layer events from app. - * - * @param {object} viewLayer The view layer. - * @private - */ - function unbindViewLayer(viewLayer) { - // stop propagating view events - viewLayer.propagateViewEvents(false); + group.addEventListener('renderstart', fireEvent); + group.addEventListener('renderend', fireEvent); + // propagate view events for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - viewLayer.removeEventListener(dwv.image.viewEventNames[j], fireEvent); + group.addEventListener(dwv.image.viewEventNames[j], fireEvent); } - // stop propagating viewLayer events - viewLayer.removeEventListener('renderstart', fireEvent); - viewLayer.removeEventListener('renderend', fireEvent); } /** * Initialise the layers. * To be called once the DICOM data has been loaded. * - * @param {object} image The image to view. - * @param {object} meta The image meta data. * @param {number} dataIndex The data index. + * @param {string} layerGroupElementId The layer group element id. * @private */ - function initialiseBaseLayers(image, meta, dataIndex) { - // view layer - var viewLayer = layerController.addViewLayer(); - // optional draw layer - if (toolboxController && toolboxController.hasTool('Draw')) { - layerController.addDrawLayer(); + function initialiseBaseLayers(dataIndex, layerGroupElementId) { + var data = dataController.get(dataIndex); + if (!data) { + throw new Error('Cannot initialise layers with data id: ' + dataIndex); + } + var layerGroup = stage.getLayerGroupWithElementId(layerGroupElementId); + if (!layerGroup) { + throw new Error('Cannot initialise layers with group id: ' + + layerGroupElementId); } - // initialise layers - layerController.initialise(image, meta, dataIndex); - - // update style - style.setBaseScale(layerController.getBaseScale()); - // bind view to app - bindViewLayer(viewLayer); - // propagate layer events - layerController.addEventListener('zoomchange', fireEvent); - layerController.addEventListener('offsetchange', fireEvent); + // add layers + addViewLayer(dataIndex, layerGroupElementId); - // listen to image changes - dataController.addEventListener('imagechange', viewLayer.onimagechange); + // update style + //style.setBaseScale(layerGroup.getBaseScale()); // initialise the toolbox if (toolboxController) { - toolboxController.init(layerController.displayToIndex); + toolboxController.init(); } } /** * Add a view layer. * - * @param {object} image The image to view. - * @param {object} meta The image meta data. * @param {number} dataIndex The data index. - */ - function addViewLayer(image, meta, dataIndex) { - // un-bind previous - unbindViewLayer(layerController.getActiveViewLayer()); + * @param {string} layerGroupElementId The layer group element id. + */ + function addViewLayer(dataIndex, layerGroupElementId) { + var data = dataController.get(dataIndex); + if (!data) { + throw new Error('Cannot initialise layers with data id: ' + dataIndex); + } + var layerGroup = stage.getLayerGroupWithElementId(layerGroupElementId); + if (!layerGroup) { + throw new Error('Cannot initialise layers with group id: ' + + layerGroupElementId); + } + var imageGeometry = data.image.getGeometry(); + + // un-bind + stage.unbindLayerGroups(); + + // create and setup view + var viewFactory = new dwv.ViewFactory(); + var view = viewFactory.create( + new dwv.dicom.DicomElementsWrapper(data.meta), + data.image); + var viewOrientation = dwv.gui.getViewOrientation( + imageGeometry, + layerGroup.getTargetOrientation() + ); + view.setOrientation(viewOrientation); + + // TODO: find another way for a default colour map + var opacity = 1; + if (dataIndex !== 0) { + view.setColourMap(dwv.image.lut.rainbow); + opacity = 0.5; + } + + // view layer + var viewLayer = layerGroup.addViewLayer(); + viewLayer.setView(view); + var size2D = imageGeometry.getSize(viewOrientation).get2D(); + var spacing2D = imageGeometry.getSpacing(viewOrientation).get2D(); + viewLayer.initialise(size2D, spacing2D, dataIndex); + viewLayer.setOpacity(opacity); + + // compensate origin difference + var diff = null; + if (dataIndex !== 0) { + var data0 = dataController.get(0); + var origin0 = data0.image.getGeometry().getOrigin(); + var origin1 = imageGeometry.getOrigin(); + diff = origin0.minus(origin1); + viewLayer.setBaseOffset(diff); + } - var viewLayer = layerController.addViewLayer(); - // initialise - viewLayer.initialise(image, meta, dataIndex); - // apply layer scale - viewLayer.resize(layerController.getScale()); // listen to image changes dataController.addEventListener('imagechange', viewLayer.onimagechange); - // bind new - bindViewLayer(viewLayer); + // bind + stage.bindLayerGroups(); + + // optional draw layer + if (toolboxController && toolboxController.hasTool('Draw')) { + var dl = layerGroup.addDrawLayer(); + dl.initialise(size2D, spacing2D, dataIndex); + dl.setPlaneHelper(viewLayer.getViewController().getPlaneHelper()); + + var vc = viewLayer.getViewController(); + // positionchange event like data + var value = [ + vc.getCurrentIndex().getValues(), + vc.getCurrentPosition().getValues() + ]; + layerGroup.updateLayersToPositionChange({value: value}); + + // compensate origin difference + if (dataIndex !== 0) { + dl.setBaseOffset(diff); + } + } + + layerGroup.fitToContainer(); } }; // namespaces var dwv = dwv || {}; +/** @namespace */ +dwv.ctrl = dwv.ctrl || {}; /* * Data (list of {image, meta}) controller. * * @class */ -dwv.DataController = function () { +dwv.ctrl.DataController = function () { /** * List of {image, meta}. @@ -1164,18 +1330,10 @@ dwv.DataController = function () { */ var data = []; - /** - * Current data index. - * - * @private - * @type {number} - */ - var currentIndex = null; - /** * Listener handler. * - * @type {object} + * @type {dwv.utils.ListenerHandler} * @private */ var listenerHandler = new dwv.utils.ListenerHandler(); @@ -1193,7 +1351,6 @@ dwv.DataController = function () { * Reset the class: empty the data storage. */ this.reset = function () { - currentIndex = null; data = []; }; @@ -1207,22 +1364,13 @@ dwv.DataController = function () { return data[index]; }; - /** - * Get the current data index. - * - * @returns {number} The index. - */ - this.getCurrentIndex = function () { - return currentIndex; - }; - /** * Set the image at a given index. * - * @param {object} image The image to set. * @param {number} index The index of the data. + * @param {dwv.image.Image} image The image to set. */ - this.setImage = function (image, index) { + this.setImage = function (index, image) { data[index].image = image; fireEvent({ type: 'imagechange', @@ -1233,11 +1381,10 @@ dwv.DataController = function () { /** * Add a new data. * - * @param {object} image The image. + * @param {dwv.image.Image} image The image. * @param {object} meta The image meta. */ this.addNew = function (image, meta) { - currentIndex = data.length; // store the new image data.push({ image: image, @@ -1248,29 +1395,43 @@ dwv.DataController = function () { /** * Update the current data. * - * @param {object} image The image. + * @param {number} index The index of the data. + * @param {dwv.image.Image} image The image. * @param {object} meta The image meta. - * @returns {number} The slice number at which the image was added. - */ - this.updateCurrent = function (image, meta) { - var currentData = data[currentIndex]; + * @param {number} timeId The time ID. + */ + this.update = function (index, image, meta, timeId) { + var dataToUpdate = data[index]; + + // handle possible timepoint + if (typeof timeId !== 'undefined') { + var size = dataToUpdate.image.getGeometry().getSize(); + // append frame for first frame (still 3D) or other frames + if ((size.length() === 3 && timeId !== 0) || + (size.length() > 3 && timeId >= size.get(3))) { + dataToUpdate.image.appendFrame(); + } + } + // add slice to current image - var sliceNb = currentData.image.appendSlice(image); + dataToUpdate.image.appendSlice(image, timeId); + // update meta data - var idKey = ''; - if (typeof meta.x00020010 !== 'undefined') { - // dicom case - idKey = 'InstanceNumber'; - } else { - idKey = 'imageUid'; + // TODO add time support + if (timeId === 0) { + var idKey = ''; + if (typeof meta.x00020010 !== 'undefined') { + // dicom case + idKey = 'InstanceNumber'; + } else { + idKey = 'imageUid'; + } + dataToUpdate.meta = dwv.utils.mergeObjects( + dataToUpdate.meta, + getMetaObject(meta), + idKey, + 'value'); } - currentData.meta = dwv.utils.mergeObjects( - currentData.meta, - getMetaObject(meta), - idKey, - 'value'); - - return sliceNb; }; /** @@ -1328,6 +1489,8 @@ dwv.DataController = function () { // namespaces var dwv = dwv || {}; dwv.draw = dwv.draw || {}; +dwv.ctrl = dwv.ctrl || {}; + /** * The Konva namespace. * @@ -1339,11 +1502,14 @@ var Konva = Konva || {}; /** * Get the draw group id for a given position. * - * @param {number} sliceNumber The slice number. - * @param {number} frameNumber The frame number. - * @returns {number} The group id. + * @param {dwv.math.Point} currentPosition The current position. + * @returns {string} The group id. + * @deprecated Use the index.toStringId instead. */ -dwv.draw.getDrawPositionGroupId = function (sliceNumber, frameNumber) { +dwv.draw.getDrawPositionGroupId = function (currentPosition) { + var sliceNumber = currentPosition.get(2); + var frameNumber = currentPosition.length() === 4 + ? currentPosition.get(3) : 0; return 'slice-' + sliceNumber + '_frame-' + frameNumber; }; @@ -1352,6 +1518,7 @@ dwv.draw.getDrawPositionGroupId = function (sliceNumber, frameNumber) { * * @param {string} groupId The group id. * @returns {object} The slice and frame number. + * @deprecated Use the dwv.math.getVectorFromStringId instead. */ dwv.draw.getPositionFromGroupId = function (groupId) { var sepIndex = groupId.indexOf('_'); @@ -1451,7 +1618,7 @@ dwv.draw.getHierarchyLog = function (layer, prefix) { * @class * @param {object} konvaLayer The draw layer. */ -dwv.DrawController = function (konvaLayer) { +dwv.ctrl.DrawController = function (konvaLayer) { // current position group id var currentPosGroupId = null; @@ -1494,16 +1661,17 @@ dwv.DrawController = function (konvaLayer) { /** * Activate the current draw layer. * - * @param {object} currentPosition The current {i,j,k} position. - * @param {number} currentFrame The current frame number. + * @param {dwv.math.Index} index The current position. + * @param {number} scrollIndex The scroll index. */ - this.activateDrawLayer = function (currentPosition, currentFrame) { - // set current position - var currentSlice = currentPosition.k; - // var currentFrame = viewController.getCurrentFrame(); + this.activateDrawLayer = function (index, scrollIndex) { + // TODO: add layer info // get and store the position group id - currentPosGroupId = dwv.draw.getDrawPositionGroupId( - currentSlice, currentFrame); + var dims = [scrollIndex]; + for (var j = 3; j < index.length(); ++j) { + dims.push(j); + } + currentPosGroupId = index.toStringId(dims); // get all position groups var posGroups = konvaLayer.getChildren(dwv.draw.isPositionNode); @@ -1525,13 +1693,14 @@ dwv.DrawController = function (konvaLayer) { /** * Get a list of drawing display details. * - * @returns {object} A list of draw details including id, slice, frame... + * @returns {Array} A list of draw details as + * {id, position, type, color, meta} */ this.getDrawDisplayDetails = function () { var list = []; var groups = konvaLayer.getChildren(); for (var j = 0, lenj = groups.length; j < lenj; ++j) { - var position = dwv.draw.getPositionFromGroupId(groups[j].id()); + var position = dwv.math.getIndexFromStringId(groups[j].id()); var collec = groups[j].getChildren(); for (var i = 0, leni = collec.length; i < leni; ++i) { var shape = collec[i].getChildren(dwv.draw.isNodeNameShape)[0]; @@ -1556,8 +1725,7 @@ dwv.DrawController = function (konvaLayer) { } list.push({ id: collec[i].id(), - slice: position.sliceNumber, - frame: position.frameNumber, + position: position.toString(), type: type, color: shape.stroke(), meta: text.meta @@ -1822,1879 +1990,1780 @@ dwv.DrawController = function (konvaLayer) { } }; -}; // class dwv.DrawController +}; // class DrawController // namespaces var dwv = dwv || {}; -dwv.gui = dwv.gui || {}; +dwv.ctrl = dwv.ctrl || {}; /** - * Layer controller. + * Load controller. * - * @param {object} containerDiv The layer div. + * @param {string} defaultCharacterSet The default character set. * @class */ -dwv.LayerController = function (containerDiv) { +dwv.ctrl.LoadController = function (defaultCharacterSet) { + // closure to self + var self = this; + // current loaders + var currentLoaders = {}; - var layers = []; + // load counter + var counter = -1; /** - * The layer scale as {x,y}. + * Get the next load id. * - * @private - * @type {object} + * @returns {number} The next id. */ - var scale = {x: 1, y: 1}; + function getNextLoadId() { + ++counter; + return counter; + } /** - * The base scale as {x,y}: all posterior scale will be on top of this one. + * Load a list of files. Can be image files or a state file. * - * @private - * @type {object} + * @param {Array} files The list of files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information */ - var baseScale = {x: 1, y: 1}; + this.loadFiles = function (files, options) { + // has been checked for emptiness. + var ext = files[0].name.split('.').pop().toLowerCase(); + if (ext === 'json') { + loadStateFile(files[0], options); + } else { + loadImageFiles(files, options); + } + }; /** - * The layer offset as {x,y}. + * Load a list of URLs. Can be image files or a state file. * - * @private - * @type {object} + * @param {Array} urls The list of urls to load. + * @param {object} options The load options: + * - requestHeaders: an array of {name, value} to use as request headers. + * - withCredentials: credentials flag to pass to the request. */ - var offset = {x: 0, y: 0}; + this.loadURLs = function (urls, options) { + // has been checked for emptiness. + var ext = urls[0].split('.').pop().toLowerCase(); + if (ext === 'json') { + loadStateUrl(urls[0], options); + } else { + loadImageUrls(urls, options); + } + }; /** - * The layer size as {x,y}. + * Load a list of ArrayBuffers. * - * @private - * @type {object} + * @param {Array} data The list of ArrayBuffers to load + * in the form of [{name: "", filename: "", data: data}]. */ - var layerSize = dwv.gui.getDivSize(containerDiv); + this.loadImageObject = function (data) { + // create IO + var memoryIO = new dwv.io.MemoryLoader(); + // load data + loadData(data, memoryIO, 'image'); + }; /** - * Active view layer index. - * - * @private - * @type {number} + * Abort the current loaders. */ - var activeViewLayerIndex = null; + this.abort = function () { + var keys = Object.keys(currentLoaders); + for (var i = 0; i < keys.length; ++i) { + currentLoaders[i].loader.abort(); + delete currentLoaders[i]; + } + }; + + // private ---------------------------------------------------------------- /** - * Active draw layer index. + * Load a list of image files. * + * @param {Array} files The list of image files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information * @private - * @type {number} */ - var activeDrawLayerIndex = null; + function loadImageFiles(files, options) { + // create IO + var fileIO = new dwv.io.FilesLoader(); + fileIO.setDefaultCharacterSet(defaultCharacterSet); + // load data + loadData(files, fileIO, 'image', options); + } /** - * Listener handler. + * Load a list of image URLs. * - * @type {object} + * @param {Array} urls The list of urls to load. + * @param {object} options The load options: + * - requestHeaders: an array of {name, value} to use as request headers. + * - withCredentials: credentials flag to pass to the request. * @private */ - var listenerHandler = new dwv.utils.ListenerHandler(); - + function loadImageUrls(urls, options) { + // create IO + var urlIO = new dwv.io.UrlsLoader(); + urlIO.setDefaultCharacterSet(defaultCharacterSet); + // load data + loadData(urls, urlIO, 'image', options); + } + /** - * Get the layer scale. + * Load a State file. * - * @returns {object} The scale as {x,y}. + * @param {string} file The state file to load. + * @param {object} options The options object. + * @private */ - this.getScale = function () { - return scale; - }; + function loadStateFile(file, options) { + // create IO + var fileIO = new dwv.io.FilesLoader(); + // load data + loadData([file], fileIO, 'state', options); + } /** - * Get the base scale. + * Load a State url. * - * @returns {object} The scale as {x,y}. + * @param {string} url The state url to load. + * @param {object} options The load options: + * - requestHeaders: an array of {name, value} to use as request headers. + * - withCredentials: credentials flag to pass to the request. + * @private */ - this.getBaseScale = function () { - return baseScale; - }; + function loadStateUrl(url, options) { + // create IO + var urlIO = new dwv.io.UrlsLoader(); + // load data + loadData([url], urlIO, 'state', options); + } /** - * Get the added scale: the scale added to the base scale + * Load a list of data. * - * @returns {object} The scale as {x,y}. + * @param {Array} data Array of data to load. + * @param {object} loader The data loader. + * @param {string} loadType The data load type: 'image' or 'state'. + * @param {object} options Options passed to the final loader. + * @private */ - this.getAddedScale = function () { - return { - x: scale.x / baseScale.x, - y: scale.y / baseScale.y + function loadData(data, loader, loadType, options) { + var eventInfo = { + loadtype: loadType, }; - }; + + // check if timepoint + var hasTimepoint = false; + if (typeof options !== 'undefined' && + typeof options.timepoint !== 'undefined') { + hasTimepoint = true; + } + + var loadId = null; + if (hasTimepoint) { + loadId = options.timepoint.dataId; + eventInfo.timepoint = options.timepoint; + } else { + loadId = getNextLoadId(); + } + eventInfo.loadid = loadId; + + // set callbacks + loader.onloadstart = function (event) { + // store loader to allow abort + currentLoaders[loadId] = { + loader: loader, + isFirstItem: true + }; + // callback + augmentCallbackEvent(self.onloadstart, eventInfo)(event); + }; + loader.onprogress = augmentCallbackEvent(self.onprogress, eventInfo); + loader.onloaditem = function (event) { + var isFirstItem = currentLoaders[loadId].isFirstItem; + var eventInfoItem = { + loadtype: loadType, + loadid: loadId, + isfirstitem: isFirstItem + }; + if (hasTimepoint) { + eventInfoItem.timepoint = options.timepoint; + } + augmentCallbackEvent(self.onloaditem, eventInfoItem)(event); + if (isFirstItem) { + currentLoaders[loadId].isFirstItem = false; + } + }; + loader.onload = augmentCallbackEvent(self.onload, eventInfo); + loader.onloadend = function (event) { + // reset current loader + delete currentLoaders[loadId]; + // callback + augmentCallbackEvent(self.onloadend, eventInfo)(event); + }; + loader.onerror = augmentCallbackEvent(self.onerror, eventInfo); + loader.onabort = augmentCallbackEvent(self.onabort, eventInfo); + // launch load + try { + loader.load(data, options); + } catch (error) { + self.onerror({ + error: error, + loadId: loadId + }); + self.onloadend({ + loadId: loadId + }); + return; + } + } /** - * Get the layer offset. + * Augment a callback event: adds loadtype to the event + * passed to a callback. * - * @returns {object} The offset as {x,y}. + * @param {object} callback The callback to update. + * @param {object} info Info object to append to the event. + * @returns {object} A function representing the modified callback. */ - this.getOffset = function () { - return offset; - }; + function augmentCallbackEvent(callback, info) { + return function (event) { + var keys = Object.keys(info); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + event[key] = info[key]; + } + callback(event); + }; + } + +}; // class LoadController + +/** + * Handle a load start event. + * Default does nothing. + * + * @param {object} _event The load start event. + */ +dwv.ctrl.LoadController.prototype.onloadstart = function (_event) {}; +/** + * Handle a load progress event. + * Default does nothing. + * + * @param {object} _event The progress event. + */ +dwv.ctrl.LoadController.prototype.onprogress = function (_event) {}; +/** + * Handle a load event. + * Default does nothing. + * + * @param {object} _event The load event fired + * when a file has been loaded successfully. + */ +dwv.ctrl.LoadController.prototype.onload = function (_event) {}; +/** + * Handle a load end event. + * Default does nothing. + * + * @param {object} _event The load end event fired + * when a file load has completed, successfully or not. + */ +dwv.ctrl.LoadController.prototype.onloadend = function (_event) {}; +/** + * Handle an error event. + * Default does nothing. + * + * @param {object} _event The error event. + */ +dwv.ctrl.LoadController.prototype.onerror = function (_event) {}; +/** + * Handle an abort event. + * Default does nothing. + * + * @param {object} _event The abort event. + */ +dwv.ctrl.LoadController.prototype.onabort = function (_event) {}; + +// namespaces +var dwv = dwv || {}; +dwv.ctrl = dwv.ctrl || {}; +/** + * Toolbox controller. + * + * @param {Array} toolList The list of tool objects. + * @class + */ +dwv.ctrl.ToolboxController = function (toolList) { /** - * Transform a display position to an index. + * Selected tool. * - * @param {dwv.Math.Point2D} point2D The point to convert. - * @returns {object} The equivalent index. + * @type {object} + * @private */ - this.displayToIndex = function (point2D) { - return { - x: point2D.x / scale.x + offset.x, - y: point2D.y / scale.y + offset.y - }; - }; + var selectedTool = null; /** - * Get the number of layers handled by this class. + * Callback store to allow attach/detach. * - * @returns {number} The number of layers. + * @type {Array} + * @private */ - this.getNumberOfLayers = function () { - return layers.length; + var callbackStore = []; + + /** + * Initialise. + */ + this.init = function () { + for (var key in toolList) { + toolList[key].init(); + } + // keydown listener + window.addEventListener('keydown', getOnMouch('window', 'keydown'), true); }; /** - * Get the active image layer. + * Get the tool list. * - * @returns {object} The layer. + * @returns {Array} The list of tool objects. */ - this.getActiveViewLayer = function () { - return layers[activeViewLayerIndex]; + this.getToolList = function () { + return toolList; }; /** - * Get the active draw layer. + * Check if a tool is in the tool list. * - * @returns {object} The layer. + * @param {string} name The name to check. + * @returns {string} The tool list element for the given name. */ - this.getActiveDrawLayer = function () { - return layers[activeDrawLayerIndex]; + this.hasTool = function (name) { + return typeof this.getToolList()[name] !== 'undefined'; }; /** - * Set the active view layer. + * Get the selected tool. * - * @param {number} index The index of the layer to set as active. + * @returns {object} The selected tool. */ - this.setActiveViewLayer = function (index) { - // un-bind previous layer - var viewLayer0 = this.getActiveViewLayer(); - if (viewLayer0) { - viewLayer0.removeEventListener( - 'slicechange', this.updatePosition); - viewLayer0.removeEventListener( - 'framechange', this.updatePosition); - } - - // set index - activeViewLayerIndex = index; - - // bind new layer - var viewLayer = this.getActiveViewLayer(); - viewLayer.addEventListener( - 'slicechange', this.updatePosition); - viewLayer.addEventListener( - 'framechange', this.updatePosition); + this.getSelectedTool = function () { + return selectedTool; }; /** - * Set the active draw layer. + * Get the selected tool event handler. * - * @param {number} index The index of the layer to set as active. + * @param {string} eventType The event type, for example + * mousedown, touchstart... + * @returns {Function} The event handler. */ - this.setActiveDrawLayer = function (index) { - activeDrawLayerIndex = index; + this.getSelectedToolEventHandler = function (eventType) { + return this.getSelectedTool()[eventType]; }; /** - * Add a view layer. + * Set the selected tool. * - * @returns {object} The created layer. + * @param {string} name The name of the tool. */ - this.addViewLayer = function () { - // layer index - var viewLayerIndex = layers.length; - // create div - var div = getNextLayerDiv(); - // prepend to container - containerDiv.append(div); - // view layer - var layer = new dwv.gui.ViewLayer(div); - // set z-index: last on top - layer.setZIndex(viewLayerIndex); - // add layer - layers.push(layer); - // mark it as active - this.setActiveViewLayer(viewLayerIndex); - // return - return layer; + this.setSelectedTool = function (name) { + // check if we have it + if (!this.hasTool(name)) { + throw new Error('Unknown tool: \'' + name + '\''); + } + // de-activate previous + if (selectedTool) { + selectedTool.activate(false); + } + // set internal var + selectedTool = toolList[name]; + // activate new tool + selectedTool.activate(true); }; /** - * Add a draw layer. + * Set the selected shape. * - * @returns {object} The created layer. + * @param {string} name The name of the shape. */ - this.addDrawLayer = function () { - // store active index - activeDrawLayerIndex = layers.length; - // create div - var div = getNextLayerDiv(); - // prepend to container - containerDiv.append(div); - // draw layer - var layer = new dwv.gui.DrawLayer(div); - // set z-index: above view + last on top - layer.setZIndex(10 + activeDrawLayerIndex); - // add layer - layers.push(layer); - // return - return layer; + this.setSelectedShape = function (name) { + this.getSelectedTool().setShapeName(name); }; /** - * Get the next layer DOM div. + * Set the selected filter. * - * @returns {object} A DOM div. + * @param {string} name The name of the filter. */ - function getNextLayerDiv() { - var div = document.createElement('div'); - div.id = 'layer' + layers.length; - div.className = 'layer'; - div.style.pointerEvents = 'none'; - return div; - } + this.setSelectedFilter = function (name) { + this.getSelectedTool().setSelectedFilter(name); + }; /** - * Empty the layer list. + * Run the selected filter. */ - this.empty = function () { - layers = []; - // reset active indices - activeViewLayerIndex = null; - activeDrawLayerIndex = null; - // clean container div - var previous = containerDiv.getElementsByClassName('layer'); - if (previous) { - while (previous.length > 0) { - previous[0].remove(); - } - } + this.runSelectedFilter = function () { + this.getSelectedTool().getSelectedFilter().run(); }; /** - * Update layers to the active view position. + * Set the tool line colour. + * + * @param {string} colour The colour. */ - this.updatePosition = function () { - var viewController = - layers[activeViewLayerIndex].getViewController(); - var pos = [ - viewController.getCurrentPosition(), - viewController.getCurrentFrame() - ]; - for (var i = 0; i < layers.length; ++i) { - if (i !== activeViewLayerIndex) { - layers[i].updatePosition(pos); - } - } + this.setLineColour = function (colour) { + this.getSelectedTool().setLineColour(colour); }; /** - * Get the fit to container scale. - * To be called once the image is loaded. + * Set the tool range. * - * @returns {number} The scale. + * @param {object} range The new range of the data. */ - this.getFitToContainerScale = function () { - // get container size - var size = this.getLayerContainerSize(); - // best fit - return Math.min( - (size.x / layerSize.x), - (size.y / layerSize.y) - ); + this.setRange = function (range) { + // seems like jquery is checking if the method exists before it + // is used... + if (this.getSelectedTool() && + this.getSelectedTool().getSelectedFilter()) { + this.getSelectedTool().getSelectedFilter().run(range); + } }; /** - * Fit the display to the size of the container. - * To be called once the image is loaded. + * Listen to layer interaction events. + * + * @param {object} layer The layer to listen to. */ - this.fitToContainer = function () { - var fitScale = this.getFitToContainerScale(); - this.resize({x: fitScale, y: fitScale}); + this.bindLayer = function (layer) { + layer.bindInteraction(); + // interaction events + var names = dwv.gui.interactionEventNames; + for (var i = 0; i < names.length; ++i) { + layer.addEventListener(names[i], + getOnMouch(layer.getId(), names[i])); + } }; /** - * Get the size available for the layer container div. + * Remove canvas mouse and touch listeners. * - * @returns {object} The available width and height as {width,height}. + * @param {object} layer The layer to stop listening to. */ - this.getLayerContainerSize = function () { - return dwv.gui.getDivSize(containerDiv); + this.unbindLayer = function (layer) { + layer.unbindInteraction(); + // interaction events + var names = dwv.gui.interactionEventNames; + for (var i = 0; i < names.length; ++i) { + layer.removeEventListener(names[i], + getOnMouch(layer.getId(), names[i])); + } }; /** - * Add scale to the layers. Scale cannot go lower than 0.1. + * Mou(se) and (T)ouch event handler. This function just determines + * the mouse/touch position relative to the canvas element. + * It then passes it to the current tool. * - * @param {object} scaleStep The scale to add. - * @param {object} center The scale center point as {x,y}. + * @param {string} layerId The layer id. + * @param {string} eventType The event type. + * @returns {object} A callback for the provided layer and event. + * @private */ - this.addScale = function (scaleStep, center) { - var newScale = { - x: Math.max(scale.x + scaleStep, 0.1), - y: Math.max(scale.y + scaleStep, 0.1) + function getOnMouch(layerId, eventType) { + // augment event with converted offsets + var augmentEventOffsets = function (event) { + // event offset(s) + var offsets = dwv.gui.getEventOffset(event); + // should have at least one offset + event._x = offsets[0].x; + event._y = offsets[0].y; + // possible second + if (offsets.length === 2) { + event._x1 = offsets[1].x; + event._y1 = offsets[1].y; + } }; - // center should stay the same: - // newOffset + center / newScale = oldOffset + center / oldScale - this.setOffset({ - x: (center.x / scale.x) + offset.x - (center.x / newScale.x), - y: (center.y / scale.y) + offset.y - (center.y / newScale.y) - }); - this.setScale(newScale); - }; + + var applySelectedTool = function (event) { + // make sure we have a tool + if (selectedTool) { + var func = selectedTool[event.type]; + if (func) { + func(event); + } + } + }; + + if (typeof callbackStore[layerId] === 'undefined') { + callbackStore[layerId] = []; + } + + if (typeof callbackStore[layerId][eventType] === 'undefined') { + var callback = null; + if (eventType === 'keydown') { + callback = function (event) { + applySelectedTool(event); + }; + } else if (eventType === 'touchend') { + callback = function (event) { + event.preventDefault(); + applySelectedTool(event); + }; + } else { + // mouse or touch events + callback = function (event) { + event.preventDefault(); + augmentEventOffsets(event); + applySelectedTool(event); + }; + } + // store callback + callbackStore[layerId][eventType] = callback; + } + + return callbackStore[layerId][eventType]; + } + +}; // class ToolboxController + +// namespaces +var dwv = dwv || {}; +dwv.ctrl = dwv.ctrl || {}; + +/** + * View controller. + * + * @param {dwv.image.View} view The associated view. + * @class + */ +dwv.ctrl.ViewController = function (view) { + // closure to self + var self = this; + // third dimension player ID (created by setInterval) + var playerID = null; + + // setup the plane helper + var planeHelper = new dwv.image.PlaneHelper( + view.getImage().getGeometry().getSpacing(), + view.getOrientation() + ); /** - * Set the layers' scale. + * Get the plane helper. * - * @param {object} newScale The scale to apply as {x,y}. - * @fires dwv.LayerController#zoomchange + * @returns {object} The helper. */ - this.setScale = function (newScale) { - scale = newScale; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].setScale(scale); - } + this.getPlaneHelper = function () { + return planeHelper; + }; - /** - * Zoom change event. - * - * @event dwv.LayerController#zoomchange - * @type {object} - * @property {Array} value The changed value. - */ - fireEvent({ - type: 'zoomchange', - value: [scale.x, scale.y], - }); + /** + * Initialise the controller. + */ + this.initialise = function () { + // set window/level to first preset + this.setWindowLevelPresetById(0); + // default position + this.setCurrentPosition2D(0, 0); }; /** - * Add translation to the layers. + * Get the window/level presets names. * - * @param {object} translation The translation as {x,y}. + * @returns {Array} The presets names. */ - this.addTranslation = function (translation) { - this.setOffset({ - x: offset.x - translation.x / scale.x, - y: offset.y - translation.y / scale.y - }); + this.getWindowLevelPresetsNames = function () { + return view.getWindowPresetsNames(); }; /** - * Set the layers' offset. + * Add window/level presets to the view. * - * @param {object} newOffset The offset as {x,y}. - * @fires dwv.LayerController#offsetchange + * @param {object} presets A preset object. + * @returns {object} The list of presets. */ - this.setOffset = function (newOffset) { - // store - offset = newOffset; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].setOffset(offset); - } + this.addWindowLevelPresets = function (presets) { + return view.addWindowPresets(presets); + }; - /** - * Offset change event. - * - * @event dwv.LayerController#offsetchange - * @type {object} - * @property {Array} value The changed value. - */ - fireEvent({ - type: 'offsetchange', - value: [offset.x, offset.y], - }); + /** + * Set the window level to the preset with the input name. + * + * @param {string} name The name of the preset to activate. + */ + this.setWindowLevelPreset = function (name) { + view.setWindowLevelPreset(name); }; /** - * Initialise the layer: set the canvas and context + * Set the window level to the preset with the input id. * - * @param {object} image The image. - * @param {object} metaData The image meta data. - * @param {number} dataIndex The data index. + * @param {number} id The id of the preset to activate. */ - this.initialise = function (image, metaData, dataIndex) { - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].initialise(image, metaData, dataIndex); - } - // first position update - this.updatePosition(); - // fit data - this.fitToContainer(); + this.setWindowLevelPresetById = function (id) { + view.setWindowLevelPresetById(id); }; /** - * Reset the stage to its initial scale and no offset. + * Check if the controller is playing. + * + * @returns {boolean} True if the controler is playing. */ - this.reset = function () { - this.setScale(baseScale); - this.setOffset({x: 0, y: 0}); + this.isPlaying = function () { + return (playerID !== null); }; /** - * Resize the layer: update the base scale and layer sizes. + * Get the current position. * - * @param {number} newScale The scale as {x,y}. + * @returns {dwv.math.Point} The position. */ - this.resize = function (newScale) { - // store - scale = { - x: scale.x * newScale.x / baseScale.x, - y: scale.y * newScale.y / baseScale.y - }; - baseScale = newScale; - - // resize container - var width = parseInt(layerSize.x * baseScale.x, 10); - var height = parseInt(layerSize.y * baseScale.y, 10); - containerDiv.style.width = width + 'px'; - containerDiv.style.height = height + 'px'; - - // resize if test passes - if (dwv.gui.canCreateCanvas(width, height)) { - // call resize and scale on layers - for (var i = 0; i < layers.length; ++i) { - layers[i].resize(baseScale); - layers[i].setScale(scale); - } - } else { - dwv.logger.warn('Cannot create a ' + width + ' * ' + height + - ' canvas, trying half the size...'); - this.resize({x: newScale.x * 0.5, y: newScale.y * 0.5}); - } + this.getCurrentPosition = function () { + return view.getCurrentPosition(); }; /** - * Draw the layer. + * Get the current index. + * + * @returns {dwv.math.Index} The current index. */ - this.draw = function () { - for (var i = 0; i < layers.length; ++i) { - layers[i].draw(); - } + this.getCurrentIndex = function () { + return view.getCurrentIndex(); }; /** - * Display the layer. + * Get the current oriented position. * - * @param {boolean} flag Whether to display the layer or not. + * @returns {dwv.math.Point} The position. */ - this.display = function (flag) { - for (var i = 0; i < layers.length; ++i) { - layers[i].display(flag); + this.getCurrentOrientedPosition = function () { + var res = view.getCurrentPosition(); + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + if (typeof view.getOrientation() !== 'undefined') { + res = view.getOrientation().getInverse().getAbs().multiplyVector3D(res); } + return res; }; /** - * Add an event listener to this class. + * Get the scroll index. * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type, will be called with the fired event. + * @returns {number} The index. */ - this.addEventListener = function (type, callback) { - listenerHandler.add(type, callback); + this.getScrollIndex = function () { + return view.getScrollIndex(); }; /** - * Remove an event listener from this class. + * Get the current scroll index value. * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type. + * @returns {object} The value. */ - this.removeEventListener = function (type, callback) { - listenerHandler.remove(type, callback); + this.getCurrentScrollIndexValue = function () { + return view.getCurrentIndex().get(view.getScrollIndex()); }; /** - * Fire an event: call all associated listeners with the input event object. + * Get the current scroll position value. * - * @param {object} event The event to fire. - * @private + * @returns {object} The value. */ - function fireEvent(event) { - listenerHandler.fireEvent(event); - } - -}; // LayerController class - -// namespaces -var dwv = dwv || {}; - -/** - * Load controller. - * - * @param {string} defaultCharacterSet The default character set. - * @class - */ -dwv.LoadController = function (defaultCharacterSet) { - // closure to self - var self = this; - // current loader - var currentLoader = null; - // Is the data mono-slice? - var isMonoSliceData = null; + this.getCurrentScrollPosition = function () { + var scrollIndex = view.getScrollIndex(); + return view.getCurrentPosition().get(scrollIndex); + }; /** - * Load a list of files. Can be image files or a state file. + * Generate display image data to be given to a canvas. * - * @param {Array} files The list of files to load. + * @param {Array} array The array to fill in. */ - this.loadFiles = function (files) { - // has been checked for emptiness. - var ext = files[0].name.split('.').pop().toLowerCase(); - if (ext === 'json') { - loadStateFile(files[0]); - } else { - loadImageFiles(files); - } + this.generateImageData = function (array) { + view.generateImageData(array); }; /** - * Load a list of URLs. Can be image files or a state file. + * Set the associated image. * - * @param {Array} urls The list of urls to load. - * @param {object} options The load options: - * - requestHeaders: an array of {name, value} to use as request headers. - * - withCredentials: credentials flag to pass to the request. + * @param {Image} img The associated image. */ - this.loadURLs = function (urls, options) { - // has been checked for emptiness. - var ext = urls[0].split('.').pop().toLowerCase(); - if (ext === 'json') { - loadStateUrl(urls[0], options); - } else { - loadImageUrls(urls, options); - } + this.setImage = function (img) { + view.setImage(img); }; /** - * Load a list of ArrayBuffers. + * Get the current spacing. * - * @param {Array} data The list of ArrayBuffers to load - * in the form of [{name: "", filename: "", data: data}]. + * @returns {Array} The 2D spacing. */ - this.loadImageObject = function (data) { - // create IO - var memoryIO = new dwv.io.MemoryLoader(); - // create options - var options = {}; - // load data - loadImageData(data, memoryIO, options); + this.get2DSpacing = function () { + var spacing = view.getImage().getGeometry().getSpacing(); + return [spacing.get(0), spacing.get(1)]; }; /** - * Abort the current load. + * Get some values from the associated image in a region. + * + * @param {dwv.math.Point2D} min Minimum point. + * @param {dwv.math.Point2D} max Maximum point. + * @returns {Array} A list of values. */ - this.abort = function () { - if (currentLoader) { - currentLoader.abort(); - currentLoader = null; + this.getImageRegionValues = function (min, max) { + var image = view.getImage(); + var orientation = view.getOrientation(); + var position = this.getCurrentIndex(); + var rescaled = true; + + // created oriented slice if needed + if (!dwv.math.isIdentityMat33(orientation)) { + // generate slice values + var sliceIter = dwv.image.getSliceIterator( + image, + position, + rescaled, + orientation + ); + var sliceValues = dwv.image.getIteratorValues(sliceIter); + // oriented geometry + var orientedSize = image.getGeometry().getSize(orientation); + var sizeValues = orientedSize.getValues(); + sizeValues[2] = 1; + var sliceSize = new dwv.image.Size(sizeValues); + var orientedSpacing = image.getGeometry().getSpacing(orientation); + var spacingValues = orientedSpacing.getValues(); + spacingValues[2] = 1; + var sliceSpacing = new dwv.image.Spacing(spacingValues); + var sliceOrigin = new dwv.math.Point3D(0, 0, 0); + var sliceGeometry = + new dwv.image.Geometry(sliceOrigin, sliceSize, sliceSpacing); + // slice image + image = new dwv.image.Image(sliceGeometry, sliceValues); + // update position + position = new dwv.math.Index([0, 0, 0]); + rescaled = false; + } + + // get region values + var iter = dwv.image.getRegionSliceIterator( + image, position, rescaled, min, max); + var values = []; + if (iter) { + values = dwv.image.getIteratorValues(iter); } + return values; }; /** - * Is the data mono-slice? + * Get some values from the associated image in variable regions. * - * @returns {boolean} True if the data only contains one slice. + * @param {Array} regions A list of regions. + * @returns {Array} A list of values. */ - this.isMonoSliceData = function () { - return isMonoSliceData; + this.getImageVariableRegionValues = function (regions) { + var iter = dwv.image.getVariableRegionSliceIterator( + view.getImage(), + this.getCurrentIndex(), + true, regions + ); + var values = []; + if (iter) { + values = dwv.image.getIteratorValues(iter); + } + return values; }; - // private ---------------------------------------------------------------- - /** - * Load a list of image files. + * Can the image values be quantified? * - * @param {Array} files The list of image files to load. - * @private + * @returns {boolean} True if possible. */ - function loadImageFiles(files) { - // create IO - var fileIO = new dwv.io.FilesLoader(); - // load data - loadImageData(files, fileIO); - } + this.canQuantifyImage = function () { + return view.getImage().canQuantify(); + }; /** - * Load a list of image URLs. + * Can window and level be applied to the data? * - * @param {Array} urls The list of urls to load. - * @param {object} options The load options: - * - requestHeaders: an array of {name, value} to use as request headers. - * - withCredentials: credentials flag to pass to the request. - * @private + * @returns {boolean} True if possible. */ - function loadImageUrls(urls, options) { - // create IO - var urlIO = new dwv.io.UrlsLoader(); - // load data - loadImageData(urls, urlIO, options); - } + this.canWindowLevel = function () { + return view.getImage().canWindowLevel(); + }; /** - * Load a State file. + * Can the data be scrolled? * - * @param {string} file The state file to load. - * @private + * @returns {boolean} True if the data has a third dimension greater than one. */ - function loadStateFile(file) { - // create IO - var fileIO = new dwv.io.FilesLoader(); - // load data - loadStateData([file], fileIO); - } + this.canScroll = function () { + return view.getImage().canScroll(view.getOrientation()); + }; /** - * Load a State url. + * Get the image size. * - * @param {string} url The state url to load. - * @param {object} options The load options: - * - requestHeaders: an array of {name, value} to use as request headers. - * - withCredentials: credentials flag to pass to the request. - * @private + * @returns {dwv.image.Size} The size. */ - function loadStateUrl(url, options) { - // create IO - var urlIO = new dwv.io.UrlsLoader(); - // load data - loadStateData([url], urlIO, options); - } + this.getImageSize = function () { + return view.getImage().getGeometry().getSize(); + }; /** - * Load a list of image data. + * Set the current position. * - * @param {Array} data Array of data to load. - * @param {object} loader The data loader. - * @param {object} options Options passed to the final loader. - * @private + * @param {dwv.math.Point} pos The position. + * @param {boolean} silent If true, does not fire a positionchange event. + * @returns {boolean} False if not in bounds. */ - function loadImageData(data, loader, options) { - // first data name - var firstName = ''; - if (typeof data[0].name !== 'undefined') { - firstName = data[0].name; - } else { - firstName = data[0]; - } - - // flag used by scroll to decide wether to activate or not - // TODO: supposing multi-slice for zip files, could not be... - isMonoSliceData = (data.length === 1 && - firstName.split('.').pop().toLowerCase() !== 'zip' && - !dwv.utils.endsWith(firstName, 'DICOMDIR') && - !dwv.utils.endsWith(firstName, '.dcmdir')); - - // set IO - var loadtype = 'image'; - loader.setDefaultCharacterSet(defaultCharacterSet); - loader.onloadstart = function (event) { - // store loader to allow abort - currentLoader = loader; - // callback - augmentCallbackEvent(self.onloadstart, loadtype)(event); - }; - loader.onprogress = augmentCallbackEvent(self.onprogress, loadtype); - loader.onloaditem = augmentCallbackEvent(self.onloaditem, loadtype); - loader.onload = augmentCallbackEvent(self.onload, loadtype); - loader.onloadend = function (event) { - // reset current loader - currentLoader = null; - // callback - augmentCallbackEvent(self.onloadend, loadtype)(event); - }; - loader.onerror = augmentCallbackEvent(self.onerror, loadtype); - loader.onabort = augmentCallbackEvent(self.onabort, loadtype); - // launch load - loader.load(data, options); - } + this.setCurrentPosition = function (pos, silent) { + return view.setCurrentPosition(pos, silent); + }; /** - * Load a State data. + * Set the current 2D (x,y) position. * - * @param {Array} data Array of data to load. - * @param {object} loader The data loader. - * @param {object} options Options passed to the final loader. - * @private + * @param {number} x The column position. + * @param {number} y The row position. + * @returns {boolean} False if not in bounds. */ - function loadStateData(data, loader, options) { - var loadtype = 'state'; - // set callbacks - loader.onloadstart = augmentCallbackEvent(self.onloadstart, loadtype); - loader.onprogress = augmentCallbackEvent(self.onprogress, loadtype); - loader.onloaditem = augmentCallbackEvent(self.onloaditem, loadtype); - loader.onload = augmentCallbackEvent(self.onload, loadtype); - loader.onloadend = augmentCallbackEvent(self.onloadend, loadtype); - loader.onerror = augmentCallbackEvent(self.onerror, loadtype); - loader.onabort = augmentCallbackEvent(self.onabort, loadtype); - // launch load - loader.load(data, options); - } + this.setCurrentPosition2D = function (x, y) { + return view.setCurrentPosition( + this.getPositionFromPlanePoint({x: x, y: y})); + }; /** - * Augment a callback event: adds loadtype to the event - * passed to a callback. + * Set the current index. * - * @param {object} callback The callback to update. - * @param {string} loadtype The loadtype property to add to the event. - * @returns {object} A function representing the modified callback. + * @param {dwv.math.Index} index The index. + * @param {boolean} silent If true, does not fire a positionchange event. + * @returns {boolean} False if not in bounds. */ - function augmentCallbackEvent(callback, loadtype) { - return function (event) { - event.loadtype = loadtype; - callback(event); - }; - } - -}; // class LoadController - -/** - * Handle a load start event. - * Default does nothing. - * - * @param {object} _event The load start event. - */ -dwv.LoadController.prototype.onloadstart = function (_event) {}; -/** - * Handle a load progress event. - * Default does nothing. - * - * @param {object} _event The progress event. - */ -dwv.LoadController.prototype.onprogress = function (_event) {}; -/** - * Handle a load event. - * Default does nothing. - * - * @param {object} _event The load event fired - * when a file has been loaded successfully. - */ -dwv.LoadController.prototype.onload = function (_event) {}; -/** - * Handle a load end event. - * Default does nothing. - * - * @param {object} _event The load end event fired - * when a file load has completed, successfully or not. - */ -dwv.LoadController.prototype.onloadend = function (_event) {}; -/** - * Handle an error event. - * Default does nothing. - * - * @param {object} _event The error event. - */ -dwv.LoadController.prototype.onerror = function (_event) {}; -/** - * Handle an abort event. - * Default does nothing. - * - * @param {object} _event The abort event. - */ -dwv.LoadController.prototype.onabort = function (_event) {}; + this.setCurrentIndex = function (index, silent) { + return view.setCurrentIndex(index, silent); + }; -// namespaces -var dwv = dwv || {}; -// external -var Konva = Konva || {}; + /** + * Get a 3D position from a plane 2D position. + * + * @param {dwv.math.Point2D} point2D The 2D position as {x,y}. + * @returns {dwv.math.Point} The 3D point. + */ + this.getPositionFromPlanePoint = function (point2D) { + // keep third direction + var k = this.getCurrentScrollIndexValue(); + var planePoint = new dwv.math.Point3D(point2D.x, point2D.y, k); + // de-orient + var point = planeHelper.getDeOrientedVector3D(planePoint); + // ~indexToWorld to not loose precision + var geometry = view.getImage().getGeometry(); + var point3D = geometry.pointToWorld(point); + // merge with current position to keep extra dimensions + return this.getCurrentPosition().mergeWith3D(point3D); + }; -/** - * State class. - * Saves: data url/path, display info. - * - * History: - * - v0.4 (dwv 0.29.0, 06/2021) - * - move drawing details into meta property - * - remove scale center and translation, add offset - * - v0.3 (dwv v0.23.0, 03/2018) - * - new drawing structure, drawings are now the full layer object and - * using toObject to avoid saving a string representation - * - new details structure: simple array of objects referenced by draw ids - * - v0.2 (dwv v0.17.0, 12/2016) - * - adds draw details: array [nslices][nframes] of detail objects - * - v0.1 (dwv v0.15.0, 07/2016) - * - adds version - * - drawings: array [nslices][nframes] with all groups - * - initial release (dwv v0.10.0, 05/2015), no version number... - * - content: window-center, window-width, position, scale, - * scaleCenter, translation, drawings - * - drawings: array [nslices] with all groups - * - * @class - */ -dwv.State = function () { /** - * Save the application state as JSON. + * Get a plane 3D position from a plane 2D position: does not compensate + * for the image origin. Needed for setting the scale center... * - * @param {object} app The associated application. - * @returns {string} The state as a JSON string. + * @param {dwv.math.Point2D} point2D The 2D position as {x,y}. + * @returns {dwv.math.Point3D} The 3D point. */ - this.toJSON = function (app) { - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); - var drawLayer = layerController.getActiveDrawLayer(); - // return a JSON string - return JSON.stringify({ - version: '0.4', - 'window-center': viewController.getWindowLevel().center, - 'window-width': viewController.getWindowLevel().width, - position: viewController.getCurrentPosition(), - scale: app.getAddedScale(), - offset: app.getOffset(), - drawings: drawLayer.getKonvaLayer().toObject(), - drawingsDetails: app.getDrawStoreDetails() - }); + this.getPlanePositionFromPlanePoint = function (point2D) { + // keep third direction + var k = this.getCurrentScrollIndexValue(); + var planePoint = new dwv.math.Point3D(point2D.x, point2D.y, k); + // de-orient + var point = planeHelper.getDeOrientedVector3D(planePoint); + // ~indexToWorld to not loose precision + var geometry = view.getImage().getGeometry(); + var spacing = geometry.getSpacing(); + return new dwv.math.Point3D( + point.getX() * spacing.get(0), + point.getY() * spacing.get(1), + point.getZ() * spacing.get(2)); }; + /** - * Load an application state from JSON. + * Get a 3D offset from a plane one. * - * @param {string} json The JSON representation of the state. - * @returns {object} The state object. + * @param {object} offset2D The plane offset as {x,y}. + * @returns {dwv.math.Vector3D} The 3D world offset. */ - this.fromJSON = function (json) { - var data = JSON.parse(json); - var res = null; - if (data.version === '0.1') { - res = readV01(data); - } else if (data.version === '0.2') { - res = readV02(data); - } else if (data.version === '0.3') { - res = readV03(data); - } else if (data.version === '0.4') { - res = readV04(data); - } else { - throw new Error('Unknown state file format version: \'' + - data.version + '\'.'); - } - return res; + this.getOffset3DFromPlaneOffset = function (offset2D) { + return planeHelper.getOffset3DFromPlaneOffset(offset2D); }; + /** - * Load an application state from JSON. + * Increment the provided dimension. * - * @param {object} app The app to apply the state to. - * @param {object} data The state data. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. */ - this.apply = function (app, data) { - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); - // display - viewController.setWindowLevel( - data['window-center'], data['window-width']); - viewController.setCurrentPosition(data.position); - // apply saved scale on top of current base one - var baseScale = app.getLayerController().getBaseScale(); - var scale = null; - var offset = null; - if (typeof data.scaleCenter !== 'undefined') { - scale = { - x: data.scale * baseScale.x, - y: data.scale * baseScale.y, - }; - // ---- transform translation (now) ---- - // Tx = -offset.x * scale.x - // => offset.x = -Tx / scale.x - // ---- transform translation (before) ---- - // origin.x = centerX - (centerX - origin.x) * (newZoomX / zoom.x); - // (zoom.x -> initial zoom = base scale, origin.x = 0) - // Tx = origin.x + (trans.x * zoom.x) - var originX = data.scaleCenter.x - data.scaleCenter.x * data.scale; - var originY = data.scaleCenter.y - data.scaleCenter.y * data.scale; - var oldTx = originX + data.translation.x * scale.x; - var oldTy = originY + data.translation.y * scale.y; - offset = { - x: -oldTx / scale.x, - y: -oldTy / scale.y - }; + this.incrementIndex = function (dim, silent) { + return view.incrementIndex(dim, silent); + }; + + /** + * Decrement the provided dimension. + * + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ + this.decrementIndex = function (dim, silent) { + return view.decrementIndex(dim, silent); + }; + + /** + * Decrement the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ + this.decrementScrollIndex = function (silent) { + return view.decrementScrollIndex(silent); + }; + + /** + * Increment the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ + this.incrementScrollIndex = function (silent) { + return view.incrementScrollIndex(silent); + }; + + /** + * Scroll play: loop through all slices. + */ + this.play = function () { + if (!this.canScroll()) { + return; + } + if (playerID === null) { + var recommendedDisplayFrameRate = + view.getImage().getMeta().RecommendedDisplayFrameRate; + var milliseconds = view.getPlaybackMilliseconds( + recommendedDisplayFrameRate); + + playerID = setInterval(function () { + // end of scroll, loop back + if (!self.incrementScrollIndex()) { + var pos1 = self.getCurrentIndex(); + var values = pos1.getValues(); + var orientation = view.getOrientation(); + values[orientation.getThirdColMajorDirection()] = 0; + var index = new dwv.math.Index(values); + var geometry = view.getImage().getGeometry(); + self.setCurrentPosition(geometry.indexToWorld(index)); + } + }, milliseconds); } else { - scale = { - x: data.scale.x * baseScale.x, - y: data.scale.y * baseScale.y - }; - offset = data.offset; + this.stop(); } - app.getLayerController().setScale(scale); - app.getLayerController().setOffset(offset); - // render to draw the view layer - app.render(); - // drawings (will draw the draw layer) - app.setDrawings(data.drawings, data.drawingsDetails); }; + /** - * Read an application state from an Object in v0.1 format. + * Stop scroll playing. + */ + this.stop = function () { + if (playerID !== null) { + clearInterval(playerID); + playerID = null; + } + }; + + /** + * Get the window/level. * - * @param {object} data The Object representation of the state. - * @returns {object} The state object. - * @private + * @returns {object} The window center and width. */ - function readV01(data) { - // update drawings - var v02DAndD = dwv.v01Tov02DrawingsAndDetails(data.drawings); - data.drawings = dwv.v02Tov03Drawings(v02DAndD.drawings).toObject(); - data.drawingsDetails = dwv.v03Tov04DrawingsDetails( - v02DAndD.drawingsDetails); - return data; - } + this.getWindowLevel = function () { + return { + width: view.getCurrentWindowLut().getWindowLevel().getWidth(), + center: view.getCurrentWindowLut().getWindowLevel().getCenter() + }; + }; + /** - * Read an application state from an Object in v0.2 format. + * Set the window/level. * - * @param {object} data The Object representation of the state. - * @returns {object} The state object. - * @private + * @param {number} wc The window center. + * @param {number} ww The window width. */ - function readV02(data) { - // update drawings - data.drawings = dwv.v02Tov03Drawings(data.drawings).toObject(); - data.drawingsDetails = dwv.v03Tov04DrawingsDetails( - dwv.v02Tov03DrawingsDetails(data.drawingsDetails)); - return data; - } + this.setWindowLevel = function (wc, ww) { + view.setWindowLevel(wc, ww); + }; + /** - * Read an application state from an Object in v0.3 format. + * Get the colour map. * - * @param {object} data The Object representation of the state. - * @returns {object} The state object. - * @private + * @returns {object} The colour map. */ - function readV03(data) { - data.drawingsDetails = dwv.v03Tov04DrawingsDetails(data.drawingsDetails); - return data; - } + this.getColourMap = function () { + return view.getColourMap(); + }; + /** - * Read an application state from an Object in v0.4 format. + * Set the colour map. * - * @param {object} data The Object representation of the state. - * @returns {object} The state object. - * @private + * @param {object} colourMap The colour map. */ - function readV04(data) { - return data; - } + this.setColourMap = function (colourMap) { + view.setColourMap(colourMap); + }; -}; // State class + /** + * Set the view per value alpha function. + * + * @param {Function} func The function. + */ + this.setViewAlphaFunction = function (func) { + view.setAlphaFunction(func); + }; + + /** + * Set the colour map from a name. + * + * @param {string} name The name of the colour map to set. + */ + this.setColourMapFromName = function (name) { + // check if we have it + if (!dwv.tool.colourMaps[name]) { + throw new Error('Unknown colour map: \'' + name + '\''); + } + // enable it + this.setColourMap(dwv.tool.colourMaps[name]); + }; + +}; // class ViewController + +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; /** - * Convert drawings from v0.2 to v0.3. - * v0.2: one layer per slice/frame - * v0.3: one layer, one group per slice. setDrawing expects the full stage + * Is the Native endianness Little Endian. * - * @param {Array} drawings An array of drawings. - * @returns {object} The layer with the converted drawings. + * @type {boolean} */ -dwv.v02Tov03Drawings = function (drawings) { - // Auxiliar variables - var group, groupShapes, parentGroup; - // Avoid errors when dropping multiple states - //drawLayer.getChildren().each(function(node){ - // node.visible(false); - //}); - - var drawLayer = new Konva.Layer({ - listening: false, - visible: true - }); - - // Get the positions-groups data - var groupDrawings = typeof drawings === 'string' - ? JSON.parse(drawings) : drawings; - // Iterate over each position-groups - for (var k = 0, lenk = groupDrawings.length; k < lenk; ++k) { - // Iterate over each frame - for (var f = 0, lenf = groupDrawings[k].length; f < lenf; ++f) { - groupShapes = groupDrawings[k][f]; - if (groupShapes.length !== 0) { - // Create position-group set as visible and append it to drawLayer - parentGroup = new Konva.Group({ - id: dwv.draw.getDrawPositionGroupId(k, f), - name: 'position-group', - visible: false - }); +dwv.dicom.isNativeLittleEndian = function () { + return new Int8Array(new Int16Array([1]).buffer)[0] > 0; +}; - // Iterate over shapes-group - for (var g = 0, leng = groupShapes.length; g < leng; ++g) { - // create the konva group - group = Konva.Node.create(groupShapes[g]); - // enforce draggable: only the shape was draggable in v0.2, - // now the whole group is. - group.draggable(true); - group.getChildren().forEach(function (gnode) { - gnode.draggable(false); - }); - // add to position group - parentGroup.add(group); - } - // add to layer - drawLayer.add(parentGroup); - } +/** + * Flip an array's endianness. + * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. + * + * @param {object} array The array to flip (modified). + */ +dwv.dicom.flipArrayEndianness = function (array) { + var blen = array.byteLength; + var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); + var bpe = array.BYTES_PER_ELEMENT; + var tmp; + for (var i = 0; i < blen; i += bpe) { + for (var j = i + bpe - 1, k = i; j > k; j--, k++) { + tmp = u8[k]; + u8[k] = u8[j]; + u8[j] = tmp; } } - - return drawLayer; }; /** - * Convert drawings from v0.1 to v0.2. - * v0.1: text on its own - * v0.2: text as part of label + * Data reader. * - * @param {Array} inputDrawings An array of drawings. - * @returns {object} The converted drawings. + * @class + * @param {Array} buffer The input array buffer. + * @param {boolean} isLittleEndian Flag to tell if the data is little + * or big endian. */ -dwv.v01Tov02DrawingsAndDetails = function (inputDrawings) { - var newDrawings = []; - var drawingsDetails = {}; +dwv.dicom.DataReader = function (buffer, isLittleEndian) { + // Set endian flag if not defined. + if (typeof isLittleEndian === 'undefined') { + isLittleEndian = true; + } - var drawGroups; - var drawGroup; - // loop over each slice - for (var k = 0, lenk = inputDrawings.length; k < lenk; ++k) { - // loop over each frame - newDrawings[k] = []; - for (var f = 0, lenf = inputDrawings[k].length; f < lenf; ++f) { - // draw group - drawGroups = inputDrawings[k][f]; - var newFrameDrawings = []; - // Iterate over shapes-group - for (var g = 0, leng = drawGroups.length; g < leng; ++g) { - // create konva group from input - drawGroup = Konva.Node.create(drawGroups[g]); - // force visible (not set in state) - drawGroup.visible(true); - // label position - var pos = {x: 0, y: 0}; - // update shape colour - var kshape = drawGroup.getChildren(function (node) { - return node.name() === 'shape'; - })[0]; - kshape.stroke(dwv.getColourHex(kshape.stroke())); - // special line case - if (drawGroup.name() === 'line-group') { - // update name - drawGroup.name('ruler-group'); - // add ticks - var ktick0 = new Konva.Line({ - points: [kshape.points()[0], - kshape.points()[1], - kshape.points()[0], - kshape.points()[1]], - name: 'shape-tick0' - }); - drawGroup.add(ktick0); - var ktick1 = new Konva.Line({ - points: [kshape.points()[2], - kshape.points()[3], - kshape.points()[2], - kshape.points()[3]], - name: 'shape-tick1' - }); - drawGroup.add(ktick1); - } - // special protractor case: update arc name - var karcs = drawGroup.getChildren(function (node) { - return node.name() === 'arc'; - }); - if (karcs.length === 1) { - karcs[0].name('shape-arc'); - } - // get its text - var ktexts = drawGroup.getChildren(function (node) { - return node.name() === 'text'; - }); - // update text: move it into a label - var ktext = new Konva.Text({ - name: 'text', - text: '' - }); - if (ktexts.length === 1) { - pos.x = ktexts[0].x(); - pos.y = ktexts[0].y(); - // remove it from the group - ktexts[0].remove(); - // use it - ktext = ktexts[0]; - } else { - // use shape position if no text - if (kshape.points().length !== 0) { - pos = {x: kshape.points()[0], - y: kshape.points()[1]}; - } - } - // create new label with text and tag - var klabel = new Konva.Label({ - x: pos.x, - y: pos.y, - name: 'label' - }); - klabel.add(ktext); - klabel.add(new Konva.Tag()); - // add label to group - drawGroup.add(klabel); - // add group to list - newFrameDrawings.push(JSON.stringify(drawGroup.toObject())); - - // create details (v0.3 format) - var textExpr = ktext.text(); - var txtLen = textExpr.length; - var quant = null; - // adapt to text with flag - if (drawGroup.name() === 'ruler-group') { - quant = { - length: { - value: parseFloat(textExpr.substr(0, txtLen - 2)), - unit: textExpr.substr(-2, 2) - } - }; - textExpr = '{length}'; - } else if (drawGroup.name() === 'ellipse-group' || - drawGroup.name() === 'rectangle-group') { - quant = { - surface: { - value: parseFloat(textExpr.substr(0, txtLen - 3)), - unit: textExpr.substr(-3, 3) - } - }; - textExpr = '{surface}'; - } else if (drawGroup.name() === 'protractor-group' || - drawGroup.name() === 'rectangle-group') { - quant = { - angle: { - value: parseFloat(textExpr.substr(0, txtLen - 1)), - unit: textExpr.substr(-1, 1) - } - }; - textExpr = '{angle}'; - } - // set details - drawingsDetails[drawGroup.id()] = { - textExpr: textExpr, - longText: '', - quant: quant - }; - - } - newDrawings[k].push(newFrameDrawings); - } - } - - return {drawings: newDrawings, drawingsDetails: drawingsDetails}; -}; - -/** - * Convert drawing details from v0.2 to v0.3. - * - v0.2: array [nslices][nframes] with all - * - v0.3: simple array of objects referenced by draw ids - * - * @param {Array} details An array of drawing details. - * @returns {object} The converted drawings. - */ -dwv.v02Tov03DrawingsDetails = function (details) { - var res = {}; - // Get the positions-groups data - var groupDetails = typeof details === 'string' - ? JSON.parse(details) : details; - // Iterate over each position-groups - for (var k = 0, lenk = groupDetails.length; k < lenk; ++k) { - // Iterate over each frame - for (var f = 0, lenf = groupDetails[k].length; f < lenf; ++f) { - // Iterate over shapes-group - for (var g = 0, leng = groupDetails[k][f].length; g < leng; ++g) { - var group = groupDetails[k][f][g]; - res[group.id] = { - textExpr: group.textExpr, - longText: group.longText, - quant: group.quant - }; - } + // Default text decoder + var defaultTextDecoder = {}; + defaultTextDecoder.decode = function (buffer) { + var result = ''; + for (var i = 0, leni = buffer.length; i < leni; ++i) { + result += String.fromCharCode(buffer[i]); } - } - return res; -}; + return result; + }; -/** - * Convert drawing details from v0.3 to v0.4. - * - v0.3: properties at group root - * - v0.4: properties in group meta object - * - * @param {Array} details An array of drawing details. - * @returns {object} The converted drawings. - */ -dwv.v03Tov04DrawingsDetails = function (details) { - var res = {}; - var keys = Object.keys(details); - // Iterate over each position-groups - for (var k = 0, lenk = keys.length; k < lenk; ++k) { - var detail = details[keys[k]]; - res[keys[k]] = { - meta: { - textExpr: detail.textExpr, - longText: detail.longText, - quantification: detail.quant - } - }; + // Text decoder + var textDecoder = defaultTextDecoder; + if (typeof window.TextDecoder !== 'undefined') { + textDecoder = new TextDecoder('iso-8859-1'); } - return res; -}; -/** - * Get the hex code of a string colour for a colour used in pre dwv v0.17. - * - * @param {string} name The name of a colour. - * @returns {string} The hex representing the colour. - */ -dwv.getColourHex = function (name) { - // default colours used in dwv version < 0.17 - var dict = { - Yellow: '#ffff00', - Red: '#ff0000', - White: '#ffffff', - Green: '#008000', - Blue: '#0000ff', - Lime: '#00ff00', - Fuchsia: '#ff00ff', - Black: '#000000' + /** + * Set the utfLabel used to construct the TextDecoder. + * + * @param {string} label The encoding label. + */ + this.setUtfLabel = function (label) { + if (typeof window.TextDecoder !== 'undefined') { + textDecoder = new TextDecoder(label); + } }; - var res = '#ffff00'; - if (typeof dict[name] !== 'undefined') { - res = dict[name]; - } - return res; -}; -// namespaces -var dwv = dwv || {}; + /** + * Is the Native endianness Little Endian. + * + * @private + * @type {boolean} + */ + var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); -/** - * Toolbox controller. - * - * @param {Array} toolList The list of tool objects. - * @class - */ -dwv.ToolboxController = function (toolList) { /** - * Point converter function + * Flag to know if the TypedArray data needs flipping. * * @private + * @type {boolean} */ - var displayToIndexConverter = null; + var needFlip = (isLittleEndian !== isNativeLittleEndian); /** - * Selected tool. + * The main data view. * - * @type {object} * @private + * @type {DataView} */ - var selectedTool = null; + var view = new DataView(buffer); /** - * Initialise. + * Read Uint16 (2 bytes) data. * - * @param {Function} converter The display to index converter. + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. */ - this.init = function (converter) { - for (var key in toolList) { - toolList[key].init(); - } - // TODO Would prefer to have this done in the addLayerListeners - displayToIndexConverter = converter; - // keydown listener - window.addEventListener('keydown', onMouch, true); + this.readUint16 = function (byteOffset) { + return view.getUint16(byteOffset, isLittleEndian); }; /** - * Get the tool list. + * Read Uint32 (4 bytes) data. * - * @returns {Array} The list of tool objects. + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. */ - this.getToolList = function () { - return toolList; + this.readUint32 = function (byteOffset) { + return view.getUint32(byteOffset, isLittleEndian); }; /** - * Check if a tool is in the tool list. + * Read Int32 (4 bytes) data. * - * @param {string} name The name to check. - * @returns {string} The tool list element for the given name. + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. */ - this.hasTool = function (name) { - return typeof this.getToolList()[name] !== 'undefined'; + this.readInt32 = function (byteOffset) { + return view.getInt32(byteOffset, isLittleEndian); }; /** - * Get the selected tool. + * Read Float32 (4 bytes) data. * - * @returns {object} The selected tool. + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. */ - this.getSelectedTool = function () { - return selectedTool; + this.readFloat32 = function (byteOffset) { + return view.getFloat32(byteOffset, isLittleEndian); }; /** - * Get the selected tool event handler. + * Read Float64 (8 bytes) data. * - * @param {string} eventType The event type, for example - * mousedown, touchstart... - * @returns {Function} The event handler. + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. */ - this.getSelectedToolEventHandler = function (eventType) { - return this.getSelectedTool()[eventType]; + this.readFloat64 = function (byteOffset) { + return view.getFloat64(byteOffset, isLittleEndian); }; /** - * Set the selected tool. + * Read binary (0/1) array. * - * @param {string} name The name of the tool. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.setSelectedTool = function (name) { - // check if we have it - if (!this.hasTool(name)) { - throw new Error('Unknown tool: \'' + name + '\''); - } - // de-activate previous - if (selectedTool) { - selectedTool.activate(false); + this.readBinaryArray = function (byteOffset, size) { + // input + var bitArray = new Uint8Array(buffer, byteOffset, size); + // result + var byteArrayLength = 8 * bitArray.length; + var data = new Uint8Array(byteArrayLength); + var bitNumber = 0; + var bitIndex = 0; + for (var i = 0; i < byteArrayLength; ++i) { + bitNumber = i % 8; + bitIndex = Math.floor(i / 8); + // see https://stackoverflow.com/questions/4854207/get-a-specific-bit-from-byte/4854257 + data[i] = 255 * ((bitArray[bitIndex] & (1 << bitNumber)) !== 0); } - // set internal var - selectedTool = toolList[name]; - // activate new tool - selectedTool.activate(true); + return data; }; /** - * Set the selected shape. + * Read Uint8 array. * - * @param {string} name The name of the shape. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.setSelectedShape = function (name) { - this.getSelectedTool().setShapeName(name); + this.readUint8Array = function (byteOffset, size) { + return new Uint8Array(buffer, byteOffset, size); }; /** - * Set the selected filter. + * Read Int8 array. * - * @param {string} name The name of the filter. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.setSelectedFilter = function (name) { - this.getSelectedTool().setSelectedFilter(name); + this.readInt8Array = function (byteOffset, size) { + return new Int8Array(buffer, byteOffset, size); }; /** - * Run the selected filter. + * Read Uint16 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.runSelectedFilter = function () { - this.getSelectedTool().getSelectedFilter().run(); + this.readUint16Array = function (byteOffset, size) { + var bpe = Uint16Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) + if (byteOffset % bpe === 0) { + data = new Uint16Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Uint16Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readUint16(byteOffset + bpe * i); + } + } + return data; }; /** - * Set the tool line colour. + * Read Int16 array. * - * @param {string} colour The colour. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.setLineColour = function (colour) { - this.getSelectedTool().setLineColour(colour); + this.readInt16Array = function (byteOffset, size) { + var bpe = Int16Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) + if (byteOffset % bpe === 0) { + data = new Int16Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Int16Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readInt16(byteOffset + bpe * i); + } + } + return data; }; /** - * Set the tool range. + * Read Uint32 array. * - * @param {object} range The new range of the data. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.setRange = function (range) { - // seems like jquery is checking if the method exists before it - // is used... - if (this.getSelectedTool() && - this.getSelectedTool().getSelectedFilter()) { - this.getSelectedTool().getSelectedFilter().run(range); + this.readUint32Array = function (byteOffset, size) { + var bpe = Uint32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Uint32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Uint32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readUint32(byteOffset + bpe * i); + } } + return data; }; /** - * Listen to layer interaction events. + * Read Int32 array. * - * @param {object} layer The layer to listen to. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.attachLayer = function (layer) { - layer.activate(); - // interaction events - var names = dwv.gui.interactionEventNames; - for (var i = 0; i < names.length; ++i) { - layer.addEventListener(names[i], onMouch); + this.readInt32Array = function (byteOffset, size) { + var bpe = Int32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Int32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Int32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readInt32(byteOffset + bpe * i); + } } + return data; }; /** - * Remove canvas mouse and touch listeners. + * Read Float32 array. * - * @param {object} layer The layer to stop listening to. + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - this.detachLayer = function (layer) { - layer.deactivate(); - // interaction events - var names = dwv.gui.interactionEventNames; - for (var i = 0; i < names.length; ++i) { - layer.removeEventListener(names[i], onMouch); + this.readFloat32Array = function (byteOffset, size) { + var bpe = Float32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Float32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Float32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readFloat32(byteOffset + bpe * i); + } } + return data; }; /** - * Mou(se) and (T)ouch event handler. This function just determines - * the mouse/touch position relative to the canvas element. - * It then passes it to the current tool. + * Read Float64 array. * - * @param {object} event The event to handle. - * @private + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. */ - function onMouch(event) { - // make sure we have a tool - if (!selectedTool) { - return; - } - - // flag not to get confused between touch and mouse - var handled = false; - // Store the event position relative to the image canvas - // in an extra member of the event: - // event._x and event._y. - var offsets = null; - var position = null; - if (event.type === 'touchstart' || - event.type === 'touchmove') { - // event offset(s) - offsets = dwv.gui.getEventOffset(event); - // should have at least one offset - event._xs = offsets[0].x; - event._ys = offsets[0].y; - position = displayToIndexConverter(offsets[0]); - event._x = parseInt(position.x, 10); - event._y = parseInt(position.y, 10); - // possible second - if (offsets.length === 2) { - event._x1s = offsets[1].x; - event._y1s = offsets[1].y; - position = displayToIndexConverter(offsets[1]); - event._x1 = parseInt(position.x, 10); - event._y1 = parseInt(position.y, 10); - } - // set handle event flag - handled = true; - } else if (event.type === 'mousemove' || - event.type === 'mousedown' || - event.type === 'mouseup' || - event.type === 'mouseout' || - event.type === 'wheel' || - event.type === 'dblclick') { - offsets = dwv.gui.getEventOffset(event); - event._xs = offsets[0].x; - event._ys = offsets[0].y; - position = displayToIndexConverter(offsets[0]); - event._x = parseInt(position.x, 10); - event._y = parseInt(position.y, 10); - // set handle event flag - handled = true; - } else if (event.type === 'keydown' || - event.type === 'touchend') { - handled = true; - } - - // Call the event handler of the curently selected tool. - if (handled) { - if (event.type !== 'keydown') { - event.preventDefault(); + this.readFloat64Array = function (byteOffset, size) { + var bpe = Float64Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) + if (byteOffset % bpe === 0) { + data = new Float64Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); } - var func = selectedTool[event.type]; - if (func) { - func(event); + } else { + data = new Float64Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readFloat64(byteOffset + bpe * i); } } - } + return data; + }; + + /** + * Read data as a string. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} nChars The number of characters to read. + * @returns {string} The read data. + */ + this.readString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return defaultTextDecoder.decode(data); + }; -}; // class dwv.ToolboxController + /** + * Read data as a 'special' string, decoding it if the + * TextDecoder is available. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} nChars The number of characters to read. + * @returns {string} The read data. + */ + this.readSpecialString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return textDecoder.decode(data); + }; + +}; // class DataReader + +/** + * Read data as an hexadecimal string. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {Array} The read data. + */ +dwv.dicom.DataReader.prototype.readHex = function (byteOffset) { + // read and convert to hex string + var str = this.readUint16(byteOffset).toString(16); + // return padded + return '0x0000'.substr(0, 6 - str.length) + str.toUpperCase(); +}; // namespaces var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; /** - * View controller. + * Data writer. * - * @param {dwv.image.View} view The associated view. * @class + * @param {Array} buffer The input array buffer. + * @param {boolean} isLittleEndian Flag to tell if the data is + * little or big endian. */ -dwv.ViewController = function (view) { - // closure to self - var self = this; - // Slice/frame player ID (created by setInterval) - var playerID = null; +dwv.dicom.DataWriter = function (buffer, isLittleEndian) { + // Set endian flag if not defined. + if (typeof isLittleEndian === 'undefined') { + isLittleEndian = true; + } - /** - * Initialise the controller. - */ - this.initialise = function () { - // set window/level to first preset - this.setWindowLevelPresetById(0); - // default position - this.setCurrentPosition2D(0, 0); - // default frame - this.setCurrentFrame(0); + // Default text encoder + var defaultTextEncoder = {}; + defaultTextEncoder.encode = function (buffer) { + var result = new Uint8Array(buffer.length); + for (var i = 0, leni = buffer.length; i < leni; ++i) { + result[i] = buffer.charCodeAt(i); + } + return result; }; + // Text encoder + var textEncoder = defaultTextEncoder; + if (typeof window.TextEncoder !== 'undefined') { + textEncoder = new TextEncoder('iso-8859-1'); + } + /** - * Get the window/level presets names. + * Set the utfLabel used to construct the TextEncoder. * - * @returns {Array} The presets names. - */ - this.getWindowLevelPresetsNames = function () { - return view.getWindowPresetsNames(); - }; - - /** - * Add window/level presets to the view. - * - * @param {object} presets A preset object. - * @returns {object} The list of presets. - */ - this.addWindowLevelPresets = function (presets) { - return view.addWindowPresets(presets); - }; - - /** - * Set the window level to the preset with the input name. - * - * @param {string} name The name of the preset to activate. + * @param {string} label The encoding label. */ - this.setWindowLevelPreset = function (name) { - view.setWindowLevelPreset(name); + this.setUtfLabel = function (label) { + if (typeof window.TextEncoder !== 'undefined') { + textEncoder = new TextEncoder(label); + } }; - /** - * Set the window level to the preset with the input id. - * - * @param {number} id The id of the preset to activate. - */ - this.setWindowLevelPresetById = function (id) { - view.setWindowLevelPresetById(id); - }; + // private DataView + var view = new DataView(buffer); - /** - * Check if the controller is playing. - * - * @returns {boolean} True is the controler is playing slices/frames. - */ - this.isPlaying = function () { - return (playerID !== null); - }; + // flag to use VR=UN for private sequences, default to false + // (mainly used in tests) + this.useUnVrForPrivateSq = false; /** - * Get the current position. + * Write Uint8 data. * - * @returns {object} The position. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.getCurrentPosition = function () { - return view.getCurrentPosition(); + this.writeUint8 = function (byteOffset, value) { + view.setUint8(byteOffset, value); + return byteOffset + Uint8Array.BYTES_PER_ELEMENT; }; /** - * Get the current spacing. + * Write Int8 data. * - * @returns {Array} The 2D spacing. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.get2DSpacing = function () { - var spacing = view.getImage().getGeometry().getSpacing(); - return [spacing.getColumnSpacing(), spacing.getRowSpacing()]; + this.writeInt8 = function (byteOffset, value) { + view.setInt8(byteOffset, value); + return byteOffset + Int8Array.BYTES_PER_ELEMENT; }; /** - * Get some values from the associated image in a region. + * Write Uint16 data. * - * @param {dwv.math.Point2D} min Minimum point. - * @param {dwv.math.Point2D} max Maximum point. - * @returns {Array} A list of values. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.getImageRegionValues = function (min, max) { - var iter = dwv.image.getRegionSliceIterator( - view.getImage(), - this.getCurrentPosition().k, - this.getCurrentFrame(), - true, min, max - ); - var values = []; - if (iter) { - values = dwv.image.getIteratorValues(iter); - } - return values; + this.writeUint16 = function (byteOffset, value) { + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; }; /** - * Get some values from the associated image in variable regions. + * Write Int16 data. * - * @param {Array} regions A list of regions. - * @returns {Array} A list of values. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.getImageVariableRegionValues = function (regions) { - var iter = dwv.image.getVariableRegionSliceIterator( - view.getImage(), - this.getCurrentPosition().k, - this.getCurrentFrame(), - true, regions - ); - var values = []; - if (iter) { - values = dwv.image.getIteratorValues(iter); - } - return values; + this.writeInt16 = function (byteOffset, value) { + view.setInt16(byteOffset, value, isLittleEndian); + return byteOffset + Int16Array.BYTES_PER_ELEMENT; }; /** - * Can the image values be quantified? + * Write Uint32 data. * - * @returns {boolean} True if possible. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.canQuantifyImage = function () { - return view.getImage().getNumberOfComponents() === 1; + this.writeUint32 = function (byteOffset, value) { + view.setUint32(byteOffset, value, isLittleEndian); + return byteOffset + Uint32Array.BYTES_PER_ELEMENT; }; /** - * Can window and level be applied to the data? + * Write Int32 data. * - * @returns {boolean} True if the data is monochrome. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.canWindowLevel = function () { - return view.getImage().getPhotometricInterpretation() - .match(/MONOCHROME/) !== null; + this.writeInt32 = function (byteOffset, value) { + view.setInt32(byteOffset, value, isLittleEndian); + return byteOffset + Int32Array.BYTES_PER_ELEMENT; }; /** - * Is the data mono-frame? + * Write Float32 data. * - * @returns {boolean} True if the data only contains one frame. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.isMonoFrameData = function () { - return view.getImage().getNumberOfFrames() === 1; + this.writeFloat32 = function (byteOffset, value) { + view.setFloat32(byteOffset, value, isLittleEndian); + return byteOffset + Float32Array.BYTES_PER_ELEMENT; }; /** - * Set the current position. + * Write Float64 data. * - * @param {object} pos The position. - * @param {boolean} silent If true, does not fire a slicechange event. - * @returns {boolean} False if not in bounds. + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. */ - this.setCurrentPosition = function (pos, silent) { - return view.setCurrentPosition(pos, silent); + this.writeFloat64 = function (byteOffset, value) { + view.setFloat64(byteOffset, value, isLittleEndian); + return byteOffset + Float64Array.BYTES_PER_ELEMENT; }; /** - * Set the current 2D (i,j) position. + * Write string data as hexadecimal. * - * @param {number} i The column index. - * @param {number} j The row index. - * @returns {boolean} False if not in bounds. + * @param {number} byteOffset The offset to start writing from. + * @param {number} str The padded hexadecimal string to write ('0x####'). + * @returns {number} The new offset position. */ - this.setCurrentPosition2D = function (i, j) { - return view.setCurrentPosition({ - i: i, - j: j, - k: view.getCurrentPosition().k - }); + this.writeHex = function (byteOffset, str) { + // remove first two chars and parse + var value = parseInt(str.substr(2), 16); + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; }; /** - * Set the current slice position. + * Write string data. * - * @param {number} k The slice index. - * @returns {boolean} False if not in bounds. + * @param {number} byteOffset The offset to start writing from. + * @param {number} str The data to write. + * @returns {number} The new offset position. */ - this.setCurrentSlice = function (k) { - return view.setCurrentPosition({ - i: view.getCurrentPosition().i, - j: view.getCurrentPosition().j, - k: k - }); + this.writeString = function (byteOffset, str) { + var data = defaultTextEncoder.encode(str); + return this.writeUint8Array(byteOffset, data); }; /** - * Increment the current slice number. + * Write data as a 'special' string, encoding it if the + * TextEncoder is available. * - * @returns {boolean} False if not in bounds. + * @param {number} byteOffset The offset to start reading from. + * @param {number} str The data to write. + * @returns {number} The new offset position. */ - this.incrementSliceNb = function () { - return self.setCurrentSlice(view.getCurrentPosition().k + 1); + this.writeSpecialString = function (byteOffset, str) { + var data = textEncoder.encode(str); + return this.writeUint8Array(byteOffset, data); }; - /** - * Decrement the current slice number. - * - * @returns {boolean} False if not in bounds. - */ - this.decrementSliceNb = function () { - return self.setCurrentSlice(view.getCurrentPosition().k - 1); - }; +}; // class DataWriter - /** - * Get the current frame. - * - * @returns {number} The frame number. - */ - this.getCurrentFrame = function () { - return view.getCurrentFrame(); - }; +/** + * Write a boolean array as binary. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeBinaryArray = function (byteOffset, array) { + if (array.length % 8 !== 0) { + throw new Error('Cannot write boolean array as binary.'); + } + var byte = null; + var val = null; + for (var i = 0, len = array.length; i < len; i += 8) { + byte = 0; + for (var j = 0; j < 8; ++j) { + val = array[i + j] === 0 ? 0 : 1; + byte += val << j; + } + byteOffset = this.writeUint8(byteOffset, byte); + } + return byteOffset; +}; - /** - * Set the current frame. - * - * @param {number} number The frame number. - * @returns {boolean} False if not in bounds. - */ - this.setCurrentFrame = function (number) { - return view.setCurrentFrame(number); - }; +/** + * Write Uint8 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint8Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint8(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Increment the current frame. - * - * @returns {boolean} False if not in bounds. - */ - this.incrementFrameNb = function () { - return view.setCurrentFrame(view.getCurrentFrame() + 1); - }; +/** + * Write Int8 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt8Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt8(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Decrement the current frame. - * - * @returns {boolean} False if not in bounds. - */ - this.decrementFrameNb = function () { - return view.setCurrentFrame(view.getCurrentFrame() - 1); - }; +/** + * Write Uint16 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint16Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint16(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Go to first slice . - * - * @returns {boolean} False if not in bounds. - * @deprecated Use the setCurrentSlice function. - */ - this.goFirstSlice = function () { - return view.setCurrentPosition({ - i: view.getCurrentPosition().i, - j: view.getCurrentPosition().j, - k: 0 - }); - }; - - /** - * - */ - this.play = function () { - if (playerID === null) { - var nSlices = view.getImage().getGeometry().getSize().getNumberOfSlices(); - var nFrames = view.getImage().getNumberOfFrames(); - var recommendedDisplayFrameRate = - view.getImage().getMeta().RecommendedDisplayFrameRate; - var milliseconds = view.getPlaybackMilliseconds( - recommendedDisplayFrameRate); - - playerID = setInterval(function () { - if (nSlices !== 1) { - if (!self.incrementSliceNb()) { - self.setCurrentSlice(0); - } - } else if (nFrames !== 1) { - if (!self.incrementFrameNb()) { - self.setCurrentFrame(0); - } - } - - }, milliseconds); - } else { - this.stop(); - } - }; - - /** - * - */ - this.stop = function () { - if (playerID !== null) { - clearInterval(playerID); - playerID = null; - } - }; - - /** - * Get the window/level. - * - * @returns {object} The window center and width. - */ - this.getWindowLevel = function () { - return { - width: view.getCurrentWindowLut().getWindowLevel().getWidth(), - center: view.getCurrentWindowLut().getWindowLevel().getCenter() - }; - }; - - /** - * Set the window/level. - * - * @param {number} wc The window center. - * @param {number} ww The window width. - */ - this.setWindowLevel = function (wc, ww) { - view.setWindowLevel(wc, ww); - }; +/** + * Write Int16 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt16Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt16(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Get the colour map. - * - * @returns {object} The colour map. - */ - this.getColourMap = function () { - return view.getColourMap(); - }; +/** + * Write Uint32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint32Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint32(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Set the colour map. - * - * @param {object} colourMap The colour map. - */ - this.setColourMap = function (colourMap) { - view.setColourMap(colourMap); - }; +/** + * Write Int32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt32Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt32(byteOffset, array[i]); + } + return byteOffset; +}; - /** - * Set the colour map from a name. - * - * @param {string} name The name of the colour map to set. - */ - this.setColourMapFromName = function (name) { - // check if we have it - if (!dwv.tool.colourMaps[name]) { - throw new Error('Unknown colour map: \'' + name + '\''); - } - // enable it - this.setColourMap(dwv.tool.colourMaps[name]); - }; +/** + * Write Float32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat32Array = function ( + byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeFloat32(byteOffset, array[i]); + } + return byteOffset; +}; -}; // class dwv.ViewController +/** + * Write Float64 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat64Array = function ( + byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeFloat64(byteOffset, array[i]); + } + return byteOffset; +}; // namespaces var dwv = dwv || {}; @@ -3767,17 +3836,10 @@ dwv.dicom.DicomElementsWrapper = function (dicomElements) { * @returns {string} The tag name. */ this.getTagName = function (tag) { - var dict = dwv.dicom.dictionary; - // dictionnary entry - var dictElement = null; - if (typeof dict[tag.group] !== 'undefined' && - typeof dict[tag.group][tag.element] !== 'undefined') { - dictElement = dict[tag.group][tag.element]; - } - // name - var name = 'Unknown Tag & Data'; - if (dictElement !== null) { - name = dictElement[2]; + var tagObj = new dwv.dicom.Tag(tag.group, tag.element); + var name = tagObj.getNameFromDictionary(); + if (name === null) { + name = tagObj.getKey2(); } return name; }; @@ -3890,18 +3952,18 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( // Polyfill for Number.isInteger. var isInteger = Number.isInteger || function (value) { return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value; + isFinite(value) && + Math.floor(value) === value; }; // TODO Support sequences. if (dicomElement.vr !== 'SQ' && - dicomElement.value.length === 1 && dicomElement.value[0] === '') { + dicomElement.value.length === 1 && dicomElement.value[0] === '') { str += '(no value available)'; } else if (dicomElement.tag.group === '0x7FE0' && - dicomElement.tag.element === '0x0010' && - dicomElement.vl === 'u/l') { + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l') { str = '(PixelSequence)'; } else if (dicomElement.vr === 'DA' && pretty) { var daValue = dicomElement.value[0]; @@ -3926,10 +3988,13 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( var tmSeconds = tmValue.length >= 6 ? tmValue.substr(4, 2) : '00'; str = tmHour + ':' + tmMinute + ':' + tmSeconds; } else { - var isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + var isOtherVR = false; + if (dicomElement.vr.length !== 0) { + isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + } var isFloatNumberVR = (dicomElement.vr === 'FL' || - dicomElement.vr === 'FD' || - dicomElement.vr === 'DS'); + dicomElement.vr === 'FD' || + dicomElement.vr === 'DS'); var valueStr = ''; for (var k = 0, lenk = dicomElement.value.length; k < lenk; ++k) { valueStr = ''; @@ -3995,30 +4060,29 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( // default prefix prefix = prefix || ''; - // get element from dictionary - var dict = dwv.dicom.dictionary; - var dictElement = null; - if (typeof dict[dicomElement.tag.group] !== 'undefined' && - typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== - 'undefined') { - dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; - } + // get tag anme from dictionary + var tag = new dwv.dicom.Tag( + dicomElement.tag.group, dicomElement.tag.element); + var tagName = tag.getNameFromDictionary(); var deSize = dicomElement.value.length; - var isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + var isOtherVR = false; + if (dicomElement.vr.length !== 0) { + isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + } // no size for delimitations if (dicomElement.tag.group === '0xFFFE' && ( dicomElement.tag.element === '0xE00D' || - dicomElement.tag.element === '0xE0DD')) { + dicomElement.tag.element === '0xE0DD')) { deSize = 0; } else if (isOtherVR) { deSize = 1; } var isPixSequence = (dicomElement.tag.group === '0x7FE0' && - dicomElement.tag.element === '0x0010' && - dicomElement.vl === 'u/l'); + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l'); var line = null; @@ -4092,8 +4156,8 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( line += ', '; line += deSize; //dictElement[1]; line += ' '; - if (dictElement !== null) { - line += dictElement[2]; + if (tagName !== null) { + line += tagName; } else { line += 'Unknown Tag & Data'; } @@ -4193,8 +4257,7 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( */ dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( group, element) { - return this.getFromKey( - dwv.dicom.getGroupElementKey(group, element)); + return this.getFromKey(new dwv.dicom.Tag(group, element).getKey()); }; /** @@ -4206,16 +4269,53 @@ dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( */ dwv.dicom.DicomElementsWrapper.prototype.getFromName = function (name) { var value = null; - var tagGE = dwv.dicom.getGroupElementFromName(name); + var tag = dwv.dicom.getTagFromDictionary(name); // check that we are not at the end of the dictionary - if (tagGE.group !== null && tagGE.element !== null) { - value = this.getFromKey( - dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element) - ); + if (tag !== null) { + value = this.getFromKey(tag.getKey()); } return value; }; +/** + * Get the pixel spacing from the different spacing tags. + * + * @returns {object} The read spacing or the default [1,1]. + */ +dwv.dicom.DicomElementsWrapper.prototype.getPixelSpacing = function () { + // default + var rowSpacing = 1; + var columnSpacing = 1; + + // 1. PixelSpacing + // 2. ImagerPixelSpacing + // 3. NominalScannedPixelSpacing + // 4. PixelAspectRatio + var keys = ['x00280030', 'x00181164', 'x00182010', 'x00280034']; + for (var k = 0; k < keys.length; ++k) { + var spacing = this.getFromKey(keys[k], true); + if (spacing && spacing.length === 2) { + rowSpacing = parseFloat(spacing[0]); + columnSpacing = parseFloat(spacing[1]); + break; + } + } + + // check + if (columnSpacing === 0) { + dwv.logger.warn('Zero column spacing.'); + columnSpacing = 1; + } + if (rowSpacing === 0) { + dwv.logger.warn('Zero row spacing.'); + rowSpacing = 1; + } + + // return + // (slice spacing will be calculated using the image position patient) + return new dwv.image.Spacing([columnSpacing, rowSpacing, 1]); +}; + /** * Get the file list from a DICOMDIR * @@ -4231,7 +4331,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { // Directory Record Sequence if (typeof elements.x00041220 === 'undefined' || - typeof elements.x00041220.value === 'undefined') { + typeof elements.x00041220.value === 'undefined') { dwv.logger.warn('No Directory Record Sequence found in DICOMDIR.'); return; } @@ -4248,7 +4348,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { for (var i = 0; i < dirSeq.length; ++i) { // Directory Record Type if (typeof dirSeq[i].x00041430 === 'undefined' || - typeof dirSeq[i].x00041430.value === 'undefined') { + typeof dirSeq[i].x00041430.value === 'undefined') { continue; } var recType = dwv.dicom.cleanString(dirSeq[i].x00041430.value[0]); @@ -4263,7 +4363,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { } else if (recType === 'IMAGE') { // Referenced File ID if (typeof dirSeq[i].x00041500 === 'undefined' || - typeof dirSeq[i].x00041500.value === 'undefined') { + typeof dirSeq[i].x00041500.value === 'undefined') { continue; } var refFileIds = dirSeq[i].x00041500.value; @@ -4292,7 +4392,23 @@ dwv.dicom = dwv.dicom || {}; * @returns {string} The version of the library. */ dwv.getVersion = function () { - return '0.29.1'; + return '0.30.0'; +}; + +/** + * Check that an input buffer includes the DICOM prefix 'DICM' + * after the 128 bytes preamble. + * Ref: [DICOM File Meta]{@link https://dicom.nema.org/dicom/2013/output/chtml/part10/chapter_7.html#sect_7.1} + * + * @param {ArrayBuffer} buffer The buffer to check. + * @returns {boolean} True if the buffer includes the prefix. + */ +dwv.dicom.hasDicomPrefix = function (buffer) { + var prefixArray = new Uint8Array(buffer, 128, 4); + var stringReducer = function (previous, current) { + return previous += String.fromCharCode(current); + }; + return prefixArray.reduce(stringReducer, '') === 'DICM'; }; /** @@ -4314,28 +4430,6 @@ dwv.dicom.cleanString = function (inputStr) { return res; }; -/** - * Is the tag group a private tag group ? - * see: http://dicom.nema.org/medical/dicom/2015a/output/html/part05.html#sect_7.8 - * - * @param {string} group The group string as '0x####' - * @returns {boolean} True if the tag group is private, - * ie if its group is an odd number. - */ -dwv.dicom.isPrivateGroup = function (group) { - var groupNumber = parseInt(group.substr(2, 6), 10); - return (groupNumber % 2) === 1; -}; - -/** - * Is the Native endianness Little Endian. - * - * @type {boolean} - */ -dwv.dicom.isNativeLittleEndian = function () { - return new Int8Array(new Int16Array([1]).buffer)[0] > 0; -}; - /** * Get the utfLabel (used by the TextDecoder) from a character set term * References: @@ -4391,1587 +4485,1548 @@ dwv.dicom.getUtfLabel = function (charSetTerm) { }; /** - * Data reader. + * Get patient orientation label in the reverse direction. * - * @class - * @param {Array} buffer The input array buffer. - * @param {boolean} isLittleEndian Flag to tell if the data is little - * or big endian. + * @param {string} ori Patient Orientation value. + * @returns {string} Reverse Orientation Label. */ -dwv.dicom.DataReader = function (buffer, isLittleEndian) { - // Set endian flag if not defined. - if (typeof isLittleEndian === 'undefined') { - isLittleEndian = true; +dwv.dicom.getReverseOrientation = function (ori) { + if (!ori) { + return null; } - - // Default text decoder - var defaultTextDecoder = {}; - defaultTextDecoder.decode = function (buffer) { - var result = ''; - for (var i = 0, leni = buffer.length; i < leni; ++i) { - result += String.fromCharCode(buffer[i]); - } - return result; + // reverse labels + var rlabels = { + L: 'R', + R: 'L', + A: 'P', + P: 'A', + H: 'F', + F: 'H' }; - // Text decoder - var textDecoder = defaultTextDecoder; - if (typeof window.TextDecoder !== 'undefined') { - textDecoder = new TextDecoder('iso-8859-1'); - } - /** - * Set the utfLabel used to construct the TextDecoder. - * - * @param {string} label The encoding label. - */ - this.setUtfLabel = function (label) { - if (typeof window.TextDecoder !== 'undefined') { - textDecoder = new TextDecoder(label); + var rori = ''; + for (var n = 0; n < ori.length; n++) { + var o = ori.substr(n, 1); + var r = rlabels[o]; + if (r) { + rori += r; } - }; + } + // return + return rori; +}; - /** - * Is the Native endianness Little Endian. - * - * @private - * @type {boolean} - */ - var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); +/** + * Tell if a given syntax is an implicit one (element with no VR). + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if an implicit syntax. + */ +dwv.dicom.isImplicitTransferSyntax = function (syntax) { + return syntax === '1.2.840.10008.1.2'; +}; - /** - * Flag to know if the TypedArray data needs flipping. - * - * @private - * @type {boolean} - */ - var needFlip = (isLittleEndian !== isNativeLittleEndian); +/** + * Tell if a given syntax is a big endian syntax. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a big endian syntax. + */ +dwv.dicom.isBigEndianTransferSyntax = function (syntax) { + return syntax === '1.2.840.10008.1.2.2'; +}; - /** - * The main data view. - * - * @private - * @type {DataView} - */ - var view = new DataView(buffer); +/** + * Tell if a given syntax is a JPEG baseline one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a jpeg baseline syntax. + */ +dwv.dicom.isJpegBaselineTransferSyntax = function (syntax) { + return syntax === '1.2.840.10008.1.2.4.50' || + syntax === '1.2.840.10008.1.2.4.51'; +}; - /** - * Flip an array's endianness. - * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. - * - * @param {object} array The array to flip (modified). - */ - this.flipArrayEndianness = function (array) { - var blen = array.byteLength; - var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); - var bpel = array.BYTES_PER_ELEMENT; - var tmp; - for (var i = 0; i < blen; i += bpel) { - for (var j = i + bpel - 1, k = i; j > k; j--, k++) { - tmp = u8[k]; - u8[k] = u8[j]; - u8[j] = tmp; - } +/** + * Tell if a given syntax is a retired JPEG one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a retired jpeg syntax. + */ +dwv.dicom.isJpegRetiredTransferSyntax = function (syntax) { + return (syntax.match(/1.2.840.10008.1.2.4.5/) !== null && + !dwv.dicom.isJpegBaselineTransferSyntax() && + !dwv.dicom.isJpegLosslessTransferSyntax()) || + syntax.match(/1.2.840.10008.1.2.4.6/) !== null; +}; + +/** + * Tell if a given syntax is a JPEG Lossless one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a jpeg lossless syntax. + */ +dwv.dicom.isJpegLosslessTransferSyntax = function (syntax) { + return syntax === '1.2.840.10008.1.2.4.57' || + syntax === '1.2.840.10008.1.2.4.70'; +}; + +/** + * Tell if a given syntax is a JPEG-LS one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a jpeg-ls syntax. + */ +dwv.dicom.isJpeglsTransferSyntax = function (syntax) { + return syntax.match(/1.2.840.10008.1.2.4.8/) !== null; +}; + +/** + * Tell if a given syntax is a JPEG 2000 one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a jpeg 2000 syntax. + */ +dwv.dicom.isJpeg2000TransferSyntax = function (syntax) { + return syntax.match(/1.2.840.10008.1.2.4.9/) !== null; +}; + +/** + * Tell if a given syntax is a RLE (Run-length encoding) one. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a RLE syntax. + */ +dwv.dicom.isRleTransferSyntax = function (syntax) { + return syntax.match(/1.2.840.10008.1.2.5/) !== null; +}; + +/** + * Tell if a given syntax needs decompression. + * + * @param {string} syntax The transfer syntax to test. + * @returns {string} The name of the decompression algorithm. + */ +dwv.dicom.getSyntaxDecompressionName = function (syntax) { + var algo = null; + if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) { + algo = 'jpeg2000'; + } else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) { + algo = 'jpeg-baseline'; + } else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { + algo = 'jpeg-lossless'; + } else if (dwv.dicom.isRleTransferSyntax(syntax)) { + algo = 'rle'; + } + return algo; +}; + +/** + * Tell if a given syntax is supported for reading. + * + * @param {string} syntax The transfer syntax to test. + * @returns {boolean} True if a supported syntax. + */ +dwv.dicom.isReadSupportedTransferSyntax = function (syntax) { + + // Unsupported: + // "1.2.840.10008.1.2.1.99": Deflated Explicit VR - Little Endian + // "1.2.840.10008.1.2.4.100": MPEG2 Image Compression + // dwv.dicom.isJpegRetiredTransferSyntax(syntax): non supported JPEG + // dwv.dicom.isJpeglsTransferSyntax(syntax): JPEG-LS + + return (syntax === '1.2.840.10008.1.2' || // Implicit VR - Little Endian + syntax === '1.2.840.10008.1.2.1' || // Explicit VR - Little Endian + syntax === '1.2.840.10008.1.2.2' || // Explicit VR - Big Endian + dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline + dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless + dwv.dicom.isJpeg2000TransferSyntax(syntax) || // JPEG 2000 + dwv.dicom.isRleTransferSyntax(syntax)); // RLE +}; + +/** + * Get the transfer syntax name. + * Reference: [UID Values]{@link http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html}. + * + * @param {string} syntax The transfer syntax. + * @returns {string} The name of the transfer syntax. + */ +dwv.dicom.getTransferSyntaxName = function (syntax) { + var name = 'Unknown'; + if (syntax === '1.2.840.10008.1.2') { + // Implicit VR - Little Endian + name = 'Little Endian Implicit'; + } else if (syntax === '1.2.840.10008.1.2.1') { + // Explicit VR - Little Endian + name = 'Little Endian Explicit'; + } else if (syntax === '1.2.840.10008.1.2.1.99') { + // Deflated Explicit VR - Little Endian + name = 'Little Endian Deflated Explicit'; + } else if (syntax === '1.2.840.10008.1.2.2') { + // Explicit VR - Big Endian + name = 'Big Endian Explicit'; + } else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) { + // JPEG baseline + if (syntax === '1.2.840.10008.1.2.4.50') { + name = 'JPEG Baseline'; + } else { // *.51 + name = 'JPEG Extended, Process 2+4'; } - }; + } else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { + // JPEG Lossless + if (syntax === '1.2.840.10008.1.2.4.57') { + name = 'JPEG Lossless, Nonhierarchical (Processes 14)'; + } else { // *.70 + name = 'JPEG Lossless, Non-hierarchical, 1st Order Prediction'; + } + } else if (dwv.dicom.isJpegRetiredTransferSyntax(syntax)) { + // Retired JPEG + name = 'Retired JPEG'; + } else if (dwv.dicom.isJpeglsTransferSyntax(syntax)) { + // JPEG-LS + name = 'JPEG-LS'; + } else if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) { + // JPEG 2000 + if (syntax === '1.2.840.10008.1.2.4.91') { + name = 'JPEG 2000 (Lossless or Lossy)'; + } else { // *.90 + name = 'JPEG 2000 (Lossless only)'; + } + } else if (syntax === '1.2.840.10008.1.2.4.100') { + // MPEG2 Image Compression + name = 'MPEG2'; + } else if (dwv.dicom.isRleTransferSyntax(syntax)) { + // RLE (lossless) + name = 'RLE'; + } + // return + return name; +}; - /** - * Read Uint16 (2 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readUint16 = function (byteOffset) { - return view.getUint16(byteOffset, isLittleEndian); - }; - /** - * Read Uint32 (4 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readUint32 = function (byteOffset) { - return view.getUint32(byteOffset, isLittleEndian); - }; - /** - * Read Int32 (4 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readInt32 = function (byteOffset) { - return view.getInt32(byteOffset, isLittleEndian); - }; - /** - * Read binary (0/1) array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readBinaryArray = function (byteOffset, size) { - // input - var bitArray = new Uint8Array(buffer, byteOffset, size); - // result - var byteArrayLength = 8 * bitArray.length; - var data = new Uint8Array(byteArrayLength); - var bitNumber = 0; - var bitIndex = 0; - for (var i = 0; i < byteArrayLength; ++i) { - bitNumber = i % 8; - bitIndex = Math.floor(i / 8); - // see https://stackoverflow.com/questions/4854207/get-a-specific-bit-from-byte/4854257 - data[i] = 255 * ((bitArray[bitIndex] & (1 << bitNumber)) !== 0); +/** + * Guess the transfer syntax from the first data element. + * See https://github.com/ivmartel/dwv/issues/188 + * (Allow to load DICOM with no DICM preamble) for more details. + * + * @param {object} firstDataElement The first data element of the DICOM header. + * @returns {object} The transfer syntax data element. + */ +dwv.dicom.guessTransferSyntax = function (firstDataElement) { + var oEightGroupBigEndian = '0x0800'; + var oEightGroupLittleEndian = '0x0008'; + // check that group is 0x0008 + var group = firstDataElement.tag.group; + if (group !== oEightGroupBigEndian && + group !== oEightGroupLittleEndian) { + throw new Error( + 'Not a valid DICOM file (no magic DICM word found' + + 'and first element not in 0x0008 group)' + ); + } + // reasonable assumption: 2 uppercase characters => explicit vr + var vr = firstDataElement.vr; + var vr0 = vr.charCodeAt(0); + var vr1 = vr.charCodeAt(1); + var implicit = (vr0 >= 65 && vr0 <= 90 && vr1 >= 65 && vr1 <= 90) + ? false : true; + // guess transfer syntax + var syntax = null; + if (group === oEightGroupLittleEndian) { + if (implicit) { + // ImplicitVRLittleEndian + syntax = '1.2.840.10008.1.2'; + } else { + // ExplicitVRLittleEndian + syntax = '1.2.840.10008.1.2.1'; } - return data; + } else { + if (implicit) { + // ImplicitVRBigEndian: impossible + throw new Error( + 'Not a valid DICOM file (no magic DICM word found' + + 'and implicit VR big endian detected)' + ); + } else { + // ExplicitVRBigEndian + syntax = '1.2.840.10008.1.2.2'; + } + } + // set transfer syntax data element + var dataElement = { + tag: { + group: '0x0002', + element: '0x0010', + name: 'x00020010', + endOffset: 4 + }, + vr: 'UI' }; - /** - * Read Uint8 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint8Array = function (byteOffset, size) { - return new Uint8Array(buffer, byteOffset, size); - }; - /** - * Read Int8 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt8Array = function (byteOffset, size) { - return new Int8Array(buffer, byteOffset, size); - }; - /** - * Read Uint16 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint16Array = function (byteOffset, size) { - var arraySize = size / Uint16Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) - if ((byteOffset % Uint16Array.BYTES_PER_ELEMENT) === 0) { - data = new Uint16Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Uint16Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getUint16((byteOffset + - Uint16Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Int16 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt16Array = function (byteOffset, size) { - var arraySize = size / Int16Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) - if ((byteOffset % Int16Array.BYTES_PER_ELEMENT) === 0) { - data = new Int16Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Int16Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getInt16((byteOffset + - Int16Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Uint32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint32Array = function (byteOffset, size) { - var arraySize = size / Uint32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Uint32Array.BYTES_PER_ELEMENT) === 0) { - data = new Uint32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Uint32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getUint32((byteOffset + - Uint32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); + dataElement.value = [syntax + ' ']; // even length + dataElement.vl = dataElement.value[0].length; + dataElement.startOffset = firstDataElement.startOffset; + dataElement.endOffset = dataElement.startOffset + dataElement.vl; + + return dataElement; +}; + +/** + * Get the appropriate TypedArray in function of arguments. + * + * @param {number} bitsAllocated The number of bites used to store + * the data: [8, 16, 32]. + * @param {number} pixelRepresentation The pixel representation, + * 0:unsigned;1:signed. + * @param {dwv.image.Size} size The size of the new array. + * @returns {Array} The good typed array. + */ +dwv.dicom.getTypedArray = function (bitsAllocated, pixelRepresentation, size) { + var res = null; + try { + if (bitsAllocated === 8) { + if (pixelRepresentation === 0) { + res = new Uint8Array(size); + } else { + res = new Int8Array(size); } - } - return data; - }; - /** - * Read Int32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt32Array = function (byteOffset, size) { - var arraySize = size / Int32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Int32Array.BYTES_PER_ELEMENT) === 0) { - data = new Int32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); + } else if (bitsAllocated === 16) { + if (pixelRepresentation === 0) { + res = new Uint16Array(size); + } else { + res = new Int16Array(size); } - } else { - data = new Int32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getInt32((byteOffset + - Int32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); + } else if (bitsAllocated === 32) { + if (pixelRepresentation === 0) { + res = new Uint32Array(size); + } else { + res = new Int32Array(size); } } - return data; - }; - /** - * Read Float32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readFloat32Array = function (byteOffset, size) { - var arraySize = size / Float32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Float32Array.BYTES_PER_ELEMENT) === 0) { - data = new Float32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Float32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getFloat32((byteOffset + - Float32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } + } catch (error) { + if (error instanceof RangeError) { + var powerOf2 = Math.floor(Math.log(size) / Math.log(2)); + dwv.logger.error('Cannot allocate array of size: ' + + size + ' (>2^' + powerOf2 + ').'); } - return data; - }; + } + return res; +}; + +/** + * Does this Value Representation (VR) have a 32bit Value Length (VL). + * Ref: [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}. + * + * @param {string} vr The data Value Representation (VR). + * @returns {boolean} True if this VR has a 32-bit VL. + */ +dwv.dicom.is32bitVLVR = function (vr) { + // added locally used 'ox' + return (vr === 'OB' || + vr === 'OW' || + vr === 'OF' || + vr === 'ox' || + vr === 'UT' || + vr === 'SQ' || + vr === 'UN'); +}; + +/** + * Get the number of bytes occupied by a data element prefix, + * i.e. without its value. + * + * @param {string} vr The Value Representation of the element. + * @param {boolean} isImplicit Does the data use implicit VR? + * @returns {number} The size of the element prefix. + * WARNING: this is valid for tags with a VR, if not sure use + * the 'isTagWithVR' function first. + * Reference: + * - [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}, + * - [Data Element implicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.5.html#table_7.5-1}. + * + * | Tag | VR | VL | Value | + * | 4 | 2 | 2 | X | -> regular explicit: 8 + X + * | 4 | 2+2 | 4 | X | -> 32bit VL: 12 + X + * + * | Tag | VL | Value | + * | 4 | 4 | X | -> implicit (32bit VL): 8 + X + * + * | Tag | Len | Value | + * | 4 | 4 | X | -> item: 8 + X + */ +dwv.dicom.getDataElementPrefixByteSize = function (vr, isImplicit) { + return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8; +}; + +/** + * DicomParser class. + * + * @class + */ +dwv.dicom.DicomParser = function () { /** - * Read Float64 array. + * The list of DICOM elements. * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. + * @type {Array} */ - this.readFloat64Array = function (byteOffset, size) { - var arraySize = size / Float64Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) - if ((byteOffset % Float64Array.BYTES_PER_ELEMENT) === 0) { - data = new Float64Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Float64Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getFloat64((byteOffset + - Float64Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; + this.dicomElements = {}; + /** - * Read data as an hexadecimal string. + * Default character set (optional). * - * @param {number} byteOffset The offset to start reading from. - * @returns {Array} The read data. + * @private + * @type {string} */ - this.readHex = function (byteOffset) { - // read and convert to hex string - var str = this.readUint16(byteOffset).toString(16); - // return padded - return '0x0000'.substr(0, 6 - str.length) + str.toUpperCase(); - }; - + var defaultCharacterSet; /** - * Read data as a string. + * Get the default character set. * - * @param {number} byteOffset The offset to start reading from. - * @param {number} nChars The number of characters to read. - * @returns {string} The read data. + * @returns {string} The default character set. */ - this.readString = function (byteOffset, nChars) { - var data = this.readUint8Array(byteOffset, nChars); - return defaultTextDecoder.decode(data); + this.getDefaultCharacterSet = function () { + return defaultCharacterSet; }; - /** - * Read data as a 'special' string, decoding it if the - * TextDecoder is available. + * Set the default character set. + * param {String} The character set. * - * @param {number} byteOffset The offset to start reading from. - * @param {number} nChars The number of characters to read. - * @returns {string} The read data. + * @param {string} characterSet The input character set. */ - this.readSpecialString = function (byteOffset, nChars) { - var data = this.readUint8Array(byteOffset, nChars); - return textDecoder.decode(data); + this.setDefaultCharacterSet = function (characterSet) { + defaultCharacterSet = characterSet; }; - }; /** - * Get the group-element pair from a tag string name. + * Get the raw DICOM data elements. * - * @param {string} tagName The tag string name. - * @returns {object} group-element pair. + * @returns {object} The raw DICOM elements. */ -dwv.dicom.getGroupElementFromName = function (tagName) { - var group = null; - var element = null; - var dict = dwv.dicom.dictionary; - var keys0 = Object.keys(dict); - var keys1 = null; - // label for nested loop break - outLabel: - // search through dictionary - for (var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) { - group = keys0[k0]; - keys1 = Object.keys(dict[group]); - for (var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) { - element = keys1[k1]; - if (dict[group][element][2] === tagName) { - break outLabel; - } - } - } - return {group: group, element: element}; +dwv.dicom.DicomParser.prototype.getRawDicomElements = function () { + return this.dicomElements; }; /** - * Immutable tag. + * Get the DICOM data elements. * - * @class - * @param {string} group The tag group. - * @param {string} element The tag element. + * @returns {object} The DICOM elements. */ -dwv.dicom.Tag = function (group, element) { - /** - * Get the tag group. - * - * @returns {string} The tag group. - */ - this.getGroup = function () { - return group; - }; - /** - * Get the tag element. - * - * @returns {string} The tag element. - */ - this.getElement = function () { - return element; - }; -}; // Tag class +dwv.dicom.DicomParser.prototype.getDicomElements = function () { + return new dwv.dicom.DicomElementsWrapper(this.dicomElements); +}; /** - * Check for Tag equality. + * Read a DICOM tag. * - * @param {object} rhs The other tag to compare to. - * @returns {boolean} True if both tags are equal. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @returns {object} An object containing the tags 'group', + * 'element' and 'name'. */ -dwv.dicom.Tag.prototype.equals = function (rhs) { - return rhs !== null && - this.getGroup() === rhs.getGroup() && - this.getElement() === rhs.getElement(); +dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) { + // group + var group = reader.readHex(offset); + offset += Uint16Array.BYTES_PER_ELEMENT; + // element + var element = reader.readHex(offset); + offset += Uint16Array.BYTES_PER_ELEMENT; + // name + var name = new dwv.dicom.Tag(group, element).getKey(); + // return + return { + group: group, + element: element, + name: name, + endOffset: offset + }; }; /** - * Check for Tag equality. + * Read an explicit item data element. * - * @param {object} rhs The other tag to compare to provided as a simple object. - * @returns {boolean} True if both tags are equal. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @param {boolean} implicit Is the DICOM VR implicit? + * @returns {object} The item data as a list of data elements. */ -dwv.dicom.Tag.prototype.equals2 = function (rhs) { - if (rhs === null || - typeof rhs.group === 'undefined' || - typeof rhs.element === 'undefined') { - return false; +dwv.dicom.DicomParser.prototype.readExplicitItemDataElement = function ( + reader, offset, implicit) { + var itemData = {}; + + // read the first item + var item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + itemData[item.tag.name] = item; + + // read until the end offset + var endOffset = offset; + offset -= item.vl; + while (offset < endOffset) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + itemData[item.tag.name] = item; } - return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); -}; -// Get the FileMetaInformationGroupLength Tag. -dwv.dicom.getFileMetaInformationGroupLengthTag = function () { - return new dwv.dicom.Tag('0x0002', '0x0000'); -}; -// Get the Item Tag. -dwv.dicom.getItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE000'); -}; -// Get the ItemDelimitationItem Tag. -dwv.dicom.getItemDelimitationItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE00D'); -}; -// Get the SequenceDelimitationItem Tag. -dwv.dicom.getSequenceDelimitationItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE0DD'); -}; -// Get the PixelData Tag. -dwv.dicom.getPixelDataTag = function () { - return new dwv.dicom.Tag('0x7FE0', '0x0010'); + return { + data: itemData, + endOffset: offset, + isSeqDelim: false + }; }; /** - * Get the group-element key used to store DICOM elements. + * Read an implicit item data element. * - * @param {number} group The DICOM group. - * @param {number} element The DICOM element. - * @returns {string} The key. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @param {boolean} implicit Is the DICOM VR implicit? + * @returns {object} The item data as a list of data elements. */ -dwv.dicom.getGroupElementKey = function (group, element) { - return 'x' + group.substr(2, 6) + element.substr(2, 6); -}; +dwv.dicom.DicomParser.prototype.readImplicitItemDataElement = function ( + reader, offset, implicit) { + var itemData = {}; -/** - * Split a group-element key used to store DICOM elements. - * - * @param {string} key The key in form "x00280102. - * @returns {object} The DICOM group and element. - */ -dwv.dicom.splitGroupElementKey = function (key) { - return {group: key.substr(1, 4), element: key.substr(5, 8)}; -}; + // read the first item + var item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; -/** - * Get patient orientation label in the reverse direction. - * - * @param {string} ori Patient Orientation value. - * @returns {string} Reverse Orientation Label. - */ -dwv.dicom.getReverseOrientation = function (ori) { - if (!ori) { - return null; + // exit if it is a sequence delimitation item + if (item.tag.name === 'xFFFEE0DD') { + return { + data: itemData, + endOffset: item.endOffset, + isSeqDelim: true + }; } - // reverse labels - var rlabels = { - L: 'R', - R: 'L', - A: 'P', - P: 'A', - H: 'F', - F: 'H' - }; - var rori = ''; - for (var n = 0; n < ori.length; n++) { - var o = ori.substr(n, 1); - var r = rlabels[o]; - if (r) { - rori += r; + // store item + itemData[item.tag.name] = item; + + // read until the item delimitation item + var isItemDelim = false; + while (!isItemDelim) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + isItemDelim = item.tag.name === 'xFFFEE00D'; + if (!isItemDelim) { + itemData[item.tag.name] = item; } } - // return - return rori; -}; - -/** - * Tell if a given syntax is an implicit one (element with no VR). - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if an implicit syntax. - */ -dwv.dicom.isImplicitTransferSyntax = function (syntax) { - return syntax === '1.2.840.10008.1.2'; -}; -/** - * Tell if a given syntax is a big endian syntax. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a big endian syntax. - */ -dwv.dicom.isBigEndianTransferSyntax = function (syntax) { - return syntax === '1.2.840.10008.1.2.2'; + return { + data: itemData, + endOffset: offset, + isSeqDelim: false + }; }; /** - * Tell if a given syntax is a JPEG baseline one. + * Read the pixel item data element. + * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}. * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a jpeg baseline syntax. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @param {boolean} implicit Is the DICOM VR implicit? + * @returns {Array} The item data as an array of data elements. */ -dwv.dicom.isJpegBaselineTransferSyntax = function (syntax) { - return syntax === '1.2.840.10008.1.2.4.50' || - syntax === '1.2.840.10008.1.2.4.51'; -}; +dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function ( + reader, offset, implicit) { + var itemData = []; -/** - * Tell if a given syntax is a retired JPEG one. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a retired jpeg syntax. - */ -dwv.dicom.isJpegRetiredTransferSyntax = function (syntax) { - return (syntax.match(/1.2.840.10008.1.2.4.5/) !== null && - !dwv.dicom.isJpegBaselineTransferSyntax() && - !dwv.dicom.isJpegLosslessTransferSyntax()) || - syntax.match(/1.2.840.10008.1.2.4.6/) !== null; -}; + // first item: basic offset table + var item = this.readDataElement(reader, offset, implicit); + var offsetTableVl = item.vl; + offset = item.endOffset; -/** - * Tell if a given syntax is a JPEG Lossless one. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a jpeg lossless syntax. - */ -dwv.dicom.isJpegLosslessTransferSyntax = function (syntax) { - return syntax === '1.2.840.10008.1.2.4.57' || - syntax === '1.2.840.10008.1.2.4.70'; -}; + // read until the sequence delimitation item + var isSeqDelim = false; + while (!isSeqDelim) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + isSeqDelim = item.tag.name === 'xFFFEE0DD'; + if (!isSeqDelim) { + itemData.push(item); + } + } -/** - * Tell if a given syntax is a JPEG-LS one. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a jpeg-ls syntax. - */ -dwv.dicom.isJpeglsTransferSyntax = function (syntax) { - return syntax.match(/1.2.840.10008.1.2.4.8/) !== null; + return { + data: itemData, + endOffset: offset, + offsetTableVl: offsetTableVl + }; }; /** - * Tell if a given syntax is a JPEG 2000 one. + * Read a DICOM data element. + * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}. * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a jpeg 2000 syntax. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @param {boolean} implicit Is the DICOM VR implicit? + * @returns {object} An object containing the element + * 'tag', 'vl', 'vr', 'data' and 'endOffset'. */ -dwv.dicom.isJpeg2000TransferSyntax = function (syntax) { - return syntax.match(/1.2.840.10008.1.2.4.9/) !== null; -}; +dwv.dicom.DicomParser.prototype.readDataElement = function ( + reader, offset, implicit) { + // Tag: group, element + var tagData = this.readTag(reader, offset); + var tag = new dwv.dicom.Tag(tagData.group, tagData.element); + offset = tagData.endOffset; -/** - * Tell if a given syntax is a RLE (Run-length encoding) one. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a RLE syntax. - */ -dwv.dicom.isRleTransferSyntax = function (syntax) { - return syntax.match(/1.2.840.10008.1.2.5/) !== null; -}; + // Value Representation (VR) + var vr = null; + var is32bitVLVR = false; + if (tag.isWithVR()) { + // implicit VR + if (implicit) { + vr = tag.getVrFromDictionary(); + if (vr === null) { + vr = 'UN'; + } + is32bitVLVR = true; + } else { + vr = reader.readString(offset, 2); + offset += 2 * Uint8Array.BYTES_PER_ELEMENT; + is32bitVLVR = dwv.dicom.is32bitVLVR(vr); + // reserved 2 bytes + if (is32bitVLVR) { + offset += 2 * Uint8Array.BYTES_PER_ELEMENT; + } + } + } else { + vr = 'UN'; + is32bitVLVR = true; + } -/** - * Tell if a given syntax needs decompression. - * - * @param {string} syntax The transfer syntax to test. - * @returns {string} The name of the decompression algorithm. - */ -dwv.dicom.getSyntaxDecompressionName = function (syntax) { - var algo = null; - if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) { - algo = 'jpeg2000'; - } else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) { - algo = 'jpeg-baseline'; - } else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { - algo = 'jpeg-lossless'; - } else if (dwv.dicom.isRleTransferSyntax(syntax)) { - algo = 'rle'; + // Value Length (VL) + var vl = 0; + if (is32bitVLVR) { + vl = reader.readUint32(offset); + offset += Uint32Array.BYTES_PER_ELEMENT; + } else { + vl = reader.readUint16(offset); + offset += Uint16Array.BYTES_PER_ELEMENT; } - return algo; -}; -/** - * Tell if a given syntax is supported for reading. - * - * @param {string} syntax The transfer syntax to test. - * @returns {boolean} True if a supported syntax. - */ -dwv.dicom.isReadSupportedTransferSyntax = function (syntax) { + // check the value of VL + var vlString = vl; + if (vl === 0xffffffff) { + vlString = 'u/l'; + vl = 0; + } - // Unsupported: - // "1.2.840.10008.1.2.1.99": Deflated Explicit VR - Little Endian - // "1.2.840.10008.1.2.4.100": MPEG2 Image Compression - // dwv.dicom.isJpegRetiredTransferSyntax(syntax): non supported JPEG - // dwv.dicom.isJpeglsTransferSyntax(syntax): JPEG-LS + // treat private tag with unknown VR and zero VL as a sequence (see #799) + //if (dwv.dicom.isPrivateGroup(tag.group) && vr === 'UN' && vl === 0) { + if (tag.isPrivate() && vr === 'UN' && vl === 0) { + vr = 'SQ'; + } - return (syntax === '1.2.840.10008.1.2' || // Implicit VR - Little Endian - syntax === '1.2.840.10008.1.2.1' || // Explicit VR - Little Endian - syntax === '1.2.840.10008.1.2.2' || // Explicit VR - Big Endian - dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline - dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless - dwv.dicom.isJpeg2000TransferSyntax(syntax) || // JPEG 2000 - dwv.dicom.isRleTransferSyntax(syntax)); // RLE -}; + var startOffset = offset; + var endOffset = startOffset + vl; -/** - * Get the transfer syntax name. - * Reference: [UID Values]{@link http://dicom.nema.org/dicom/2013/output/chtml/part06/chapter_A.html}. - * - * @param {string} syntax The transfer syntax. - * @returns {string} The name of the transfer syntax. - */ -dwv.dicom.getTransferSyntaxName = function (syntax) { - var name = 'Unknown'; - if (syntax === '1.2.840.10008.1.2') { - // Implicit VR - Little Endian - name = 'Little Endian Implicit'; - } else if (syntax === '1.2.840.10008.1.2.1') { - // Explicit VR - Little Endian - name = 'Little Endian Explicit'; - } else if (syntax === '1.2.840.10008.1.2.1.99') { - // Deflated Explicit VR - Little Endian - name = 'Little Endian Deflated Explicit'; - } else if (syntax === '1.2.840.10008.1.2.2') { - // Explicit VR - Big Endian - name = 'Big Endian Explicit'; - } else if (dwv.dicom.isJpegBaselineTransferSyntax(syntax)) { - // JPEG baseline - if (syntax === '1.2.840.10008.1.2.4.50') { - name = 'JPEG Baseline'; - } else { // *.51 - name = 'JPEG Extended, Process 2+4'; - } - } else if (dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { - // JPEG Lossless - if (syntax === '1.2.840.10008.1.2.4.57') { - name = 'JPEG Lossless, Nonhierarchical (Processes 14)'; - } else { // *.70 - name = 'JPEG Lossless, Non-hierarchical, 1st Order Prediction'; - } - } else if (dwv.dicom.isJpegRetiredTransferSyntax(syntax)) { - // Retired JPEG - name = 'Retired JPEG'; - } else if (dwv.dicom.isJpeglsTransferSyntax(syntax)) { - // JPEG-LS - name = 'JPEG-LS'; - } else if (dwv.dicom.isJpeg2000TransferSyntax(syntax)) { - // JPEG 2000 - if (syntax === '1.2.840.10008.1.2.4.91') { - name = 'JPEG 2000 (Lossless or Lossy)'; - } else { // *.90 - name = 'JPEG 2000 (Lossless only)'; + // read sequence elements + var data = null; + if (dwv.dicom.isPixelDataTag(tag) && vlString === 'u/l') { + // pixel data sequence (implicit) + var pixItemData = this.readPixelItemDataElement(reader, offset, implicit); + offset = pixItemData.endOffset; + startOffset += pixItemData.offsetTableVl; + data = pixItemData.data; + endOffset = offset; + } else if (vr === 'SQ') { + // sequence + data = []; + var itemData; + if (vlString !== 'u/l') { + // explicit VR sequence + if (vl !== 0) { + // read until the end offset + var sqEndOffset = offset + vl; + while (offset < sqEndOffset) { + itemData = this.readExplicitItemDataElement(reader, offset, implicit); + data.push(itemData.data); + offset = itemData.endOffset; + } + endOffset = offset; + } + } else { + // implicit VR sequence + // read until the sequence delimitation item + var isSeqDelim = false; + while (!isSeqDelim) { + itemData = this.readImplicitItemDataElement(reader, offset, implicit); + isSeqDelim = itemData.isSeqDelim; + offset = itemData.endOffset; + // do not store the delimitation item + if (!isSeqDelim) { + data.push(itemData.data); + } + } + endOffset = offset; } - } else if (syntax === '1.2.840.10008.1.2.4.100') { - // MPEG2 Image Compression - name = 'MPEG2'; - } else if (dwv.dicom.isRleTransferSyntax(syntax)) { - // RLE (lossless) - name = 'RLE'; } + // return - return name; + var element = { + tag: tagData, + vr: vr, + vl: vlString, + startOffset: startOffset, + endOffset: endOffset + }; + if (data) { + element.elements = data; + } + return element; }; /** - * Get the appropriate TypedArray in function of arguments. + * Interpret the data of an element. * - * @param {number} bitsAllocated The number of bites used to store - * the data: [8, 16, 32]. - * @param {number} pixelRepresentation The pixel representation, - * 0:unsigned;1:signed. - * @param {dwv.image.Size} size The size of the new array. - * @returns {Array} The good typed array. + * @param {object} element The data element. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} pixelRepresentation PixelRepresentation 0->unsigned, + * 1->signed (needed for pixel data or VR=xs). + * @param {number} bitsAllocated Bits allocated (needed for pixel data). + * @returns {object} The interpreted data. */ -dwv.dicom.getTypedArray = function (bitsAllocated, pixelRepresentation, size) { - var res = null; - if (bitsAllocated === 8) { - if (pixelRepresentation === 0) { - res = new Uint8Array(size); - } else { - res = new Int8Array(size); +dwv.dicom.DicomParser.prototype.interpretElement = function ( + element, reader, pixelRepresentation, bitsAllocated) { + + var tag = element.tag; + var vl = element.vl; + var vr = element.vr; + var offset = element.startOffset; + + // data + var data = null; + var isPixelDataTag = dwv.dicom.isPixelDataTag( + new dwv.dicom.Tag(tag.group, tag.element)); + if (isPixelDataTag && vl === 'u/l') { + // implicit pixel data sequence + data = []; + for (var j = 0; j < element.elements.length; ++j) { + data.push(this.interpretElement( + element.elements[j], reader, + pixelRepresentation, bitsAllocated)); } - } else if (bitsAllocated === 16) { - if (pixelRepresentation === 0) { - res = new Uint16Array(size); + } else if (isPixelDataTag && + (vr === 'OB' || vr === 'OW' || vr === 'OF' || vr === 'ox')) { + // check bits allocated and VR + if (bitsAllocated === 8 && vr === 'OW') { + dwv.logger.warn( + 'Reading DICOM pixel data with vr=OW' + + ' and bitsAllocated=8 (should be 16).' + ); + } + if (bitsAllocated === 16 && vr === 'OB') { + dwv.logger.warn( + 'Reading DICOM pixel data with vr=OB' + + ' and bitsAllocated=16 (should be 8).' + ); + } + // read + data = []; + if (bitsAllocated === 1) { + data.push(reader.readBinaryArray(offset, vl)); + } else if (bitsAllocated === 8) { + if (pixelRepresentation === 0) { + data.push(reader.readUint8Array(offset, vl)); + } else { + data.push(reader.readInt8Array(offset, vl)); + } + } else if (bitsAllocated === 16) { + if (pixelRepresentation === 0) { + data.push(reader.readUint16Array(offset, vl)); + } else { + data.push(reader.readInt16Array(offset, vl)); + } + } else if (bitsAllocated === 32) { + if (pixelRepresentation === 0) { + data.push(reader.readUint32Array(offset, vl)); + } else { + data.push(reader.readInt32Array(offset, vl)); + } + } else if (bitsAllocated === 64) { + if (pixelRepresentation === 0) { + data.push(reader.readUint64Array(offset, vl)); + } else { + data.push(reader.readInt64Array(offset, vl)); + } } else { - res = new Int16Array(size); + throw new Error('Unsupported bits allocated: ' + bitsAllocated); } - } else if (bitsAllocated === 32) { + } else if (vr === 'OB') { + data = reader.readUint8Array(offset, vl); + } else if (vr === 'OW') { + data = reader.readUint16Array(offset, vl); + } else if (vr === 'OF') { + data = reader.readUint32Array(offset, vl); + } else if (vr === 'OD') { + data = reader.readUint64Array(offset, vl); + } else if (vr === 'US') { + data = reader.readUint16Array(offset, vl); + } else if (vr === 'UL') { + data = reader.readUint32Array(offset, vl); + } else if (vr === 'SS') { + data = reader.readInt16Array(offset, vl); + } else if (vr === 'SL') { + data = reader.readInt32Array(offset, vl); + } else if (vr === 'FL') { + data = reader.readFloat32Array(offset, vl); + } else if (vr === 'FD') { + data = reader.readFloat64Array(offset, vl); + } else if (vr === 'xs') { if (pixelRepresentation === 0) { - res = new Uint32Array(size); + data = reader.readUint16Array(offset, vl); } else { - res = new Int32Array(size); + data = reader.readInt16Array(offset, vl); } - } - return res; -}; - -/** - * Does this Value Representation (VR) have a 32bit Value Length (VL). - * Ref: [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}. - * - * @param {string} vr The data Value Representation (VR). - * @returns {boolean} True if this VR has a 32-bit VL. - */ -dwv.dicom.is32bitVLVR = function (vr) { - // added locally used 'ox' - return (vr === 'OB' || - vr === 'OW' || - vr === 'OF' || - vr === 'ox' || - vr === 'UT' || - vr === 'SQ' || - vr === 'UN'); + } else if (vr === 'AT') { + // attribute + var raw = reader.readUint16Array(offset, vl); + data = []; + for (var i = 0, leni = raw.length; i < leni; i += 2) { + var stri = raw[i].toString(16); + var stri1 = raw[i + 1].toString(16); + var str = '('; + str += '0000'.substr(0, 4 - stri.length) + stri.toUpperCase(); + str += ','; + str += '0000'.substr(0, 4 - stri1.length) + stri1.toUpperCase(); + str += ')'; + data.push(str); + } + } else if (vr === 'UN') { + // not available + data = reader.readUint8Array(offset, vl); + } else if (vr === 'SQ') { + // sequence + data = []; + for (var k = 0; k < element.elements.length; ++k) { + var item = element.elements[k]; + var itemData = {}; + var keys = Object.keys(item); + for (var l = 0; l < keys.length; ++l) { + var subElement = item[keys[l]]; + subElement.value = this.interpretElement( + subElement, reader, + pixelRepresentation, bitsAllocated); + itemData[keys[l]] = subElement; + } + data.push(itemData); + } + } else { + // raw + if (vr === 'SH' || vr === 'LO' || vr === 'ST' || + vr === 'PN' || vr === 'LT' || vr === 'UT') { + data = reader.readSpecialString(offset, vl); + } else { + data = reader.readString(offset, vl); + } + data = data.split('\\'); + } + + return data; }; /** - * Does this tag have a VR. - * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. + * Interpret the data of a list of elements. * - * @param {string} group The tag group. - * @param {string} element The tag element. - * @returns {boolean} True if this tar has a VR. + * @param {Array} elements A list of data elements. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} pixelRepresentation PixelRepresentation 0->unsigned, + * 1->signed. + * @param {number} bitsAllocated Bits allocated. */ -dwv.dicom.isTagWithVR = function (group, element) { - return !(group === '0xFFFE' && - (element === '0xE000' || element === '0xE00D' || element === '0xE0DD') - ); -}; +dwv.dicom.DicomParser.prototype.interpret = function ( + elements, reader, + pixelRepresentation, bitsAllocated) { + var keys = Object.keys(elements); + for (var i = 0; i < keys.length; ++i) { + var element = elements[keys[i]]; + if (typeof element.value === 'undefined') { + element.value = this.interpretElement( + element, reader, pixelRepresentation, bitsAllocated); + } + } +}; /** - * Get the number of bytes occupied by a data element prefix, - * i.e. without its value. - * - * @param {string} vr The Value Representation of the element. - * @param {boolean} isImplicit Does the data use implicit VR? - * @returns {number} The size of the element prefix. - * WARNING: this is valid for tags with a VR, if not sure use - * the 'isTagWithVR' function first. - * Reference: - * - [Data Element explicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_7.html#table_7.1-1}, - * - [Data Element implicit]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_7.5.html#table_7.5-1}. - * - * | Tag | VR | VL | Value | - * | 4 | 2 | 2 | X | -> regular explicit: 8 + X - * | 4 | 2+2 | 4 | X | -> 32bit VL: 12 + X - * - * | Tag | VL | Value | - * | 4 | 4 | X | -> implicit (32bit VL): 8 + X + * Parse the complete DICOM file (given as input to the class). + * Fills in the member object 'dicomElements'. * - * | Tag | Len | Value | - * | 4 | 4 | X | -> item: 8 + X + * @param {object} buffer The input array buffer. */ -dwv.dicom.getDataElementPrefixByteSize = function (vr, isImplicit) { - return isImplicit ? 8 : dwv.dicom.is32bitVLVR(vr) ? 12 : 8; +dwv.dicom.DicomParser.prototype.parse = function (buffer) { + var offset = 0; + var syntax = ''; + var dataElement = null; + // default readers + var metaReader = new dwv.dicom.DataReader(buffer); + var dataReader = new dwv.dicom.DataReader(buffer); + + // 128 -> 132: magic word + offset = 128; + var magicword = metaReader.readString(offset, 4); + offset += 4 * Uint8Array.BYTES_PER_ELEMENT; + if (magicword === 'DICM') { + // 0x0002, 0x0000: FileMetaInformationGroupLength + dataElement = this.readDataElement(metaReader, offset, false); + dataElement.value = this.interpretElement(dataElement, metaReader); + // increment offset + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + // get meta length + var metaLength = parseInt(dataElement.value[0], 10); + + // meta elements + var metaEnd = offset + metaLength; + while (offset < metaEnd) { + // get the data element + dataElement = this.readDataElement(metaReader, offset, false); + offset = dataElement.endOffset; + // store the data element + this.dicomElements[dataElement.tag.name] = dataElement; + } + + // check the TransferSyntaxUID (has to be there!) + dataElement = this.dicomElements.x00020010; + if (typeof dataElement === 'undefined') { + throw new Error('Not a valid DICOM file (no TransferSyntaxUID found)'); + } + dataElement.value = this.interpretElement(dataElement, metaReader); + syntax = dwv.dicom.cleanString(dataElement.value[0]); + + } else { + // read first element + dataElement = this.readDataElement(dataReader, 0, false); + // guess transfer syntax + var tsElement = dwv.dicom.guessTransferSyntax(dataElement); + // store + this.dicomElements[tsElement.tag.name] = tsElement; + syntax = dwv.dicom.cleanString(tsElement.value[0]); + // reset offset + offset = 0; + } + + // check transfer syntax support + if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) { + throw new Error('Unsupported DICOM transfer syntax: \'' + syntax + + '\' (' + dwv.dicom.getTransferSyntaxName(syntax) + ')'); + } + + // set implicit flag + var implicit = false; + if (dwv.dicom.isImplicitTransferSyntax(syntax)) { + implicit = true; + } + + // Big Endian + if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { + dataReader = new dwv.dicom.DataReader(buffer, false); + } + + // default character set + if (typeof this.getDefaultCharacterSet() !== 'undefined') { + dataReader.setUtfLabel(this.getDefaultCharacterSet()); + } + + // DICOM data elements + while (offset < buffer.byteLength) { + // get the data element + dataElement = this.readDataElement(dataReader, offset, implicit); + // increment offset + offset = dataElement.endOffset; + // store the data element + if (typeof this.dicomElements[dataElement.tag.name] === 'undefined') { + this.dicomElements[dataElement.tag.name] = dataElement; + } else { + dwv.logger.warn('Not saving duplicate tag: ' + dataElement.tag.name); + } + } + + // safety checks... + if (isNaN(offset)) { + throw new Error('Problem while parsing, bad offset'); + } + if (buffer.byteLength !== offset) { + dwv.logger.warn('Did not reach the end of the buffer: ' + + offset + ' != ' + buffer.byteLength); + } + + //------------------------------------------------- + // values needed for data interpretation + + // PixelRepresentation 0->unsigned, 1->signed + var pixelRepresentation = 0; + dataElement = this.dicomElements.x00280103; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + pixelRepresentation = dataElement.value[0]; + } else { + dwv.logger.warn( + 'Reading DICOM pixel data with default pixelRepresentation.'); + } + + // BitsAllocated + var bitsAllocated = 16; + dataElement = this.dicomElements.x00280100; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + bitsAllocated = dataElement.value[0]; + } else { + dwv.logger.warn('Reading DICOM pixel data with default bitsAllocated.'); + } + + // character set + dataElement = this.dicomElements.x00080005; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + var charSetTerm; + if (dataElement.value.length === 1) { + charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); + } else { + charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); + dwv.logger.warn('Unsupported character set with code extensions: \'' + + charSetTerm + '\'.'); + } + dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); + } + + // interpret the dicom elements + this.interpret( + this.dicomElements, dataReader, + pixelRepresentation, bitsAllocated + ); + + // handle fragmented pixel buffer + // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html + // (third note, "Depending on the transfer syntax...") + dataElement = this.dicomElements.x7FE00010; + if (typeof dataElement !== 'undefined') { + if (dataElement.vl === 'u/l') { + var numberOfFrames = 1; + if (typeof this.dicomElements.x00280008 !== 'undefined') { + numberOfFrames = dwv.dicom.cleanString( + this.dicomElements.x00280008.value[0]); + } + var pixItems = dataElement.value; + if (pixItems.length > 1 && pixItems.length > numberOfFrames) { + // concatenate pixel data items + // concat does not work on typed arrays + //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); + // manual concat... + var nItemPerFrame = pixItems.length / numberOfFrames; + var newPixItems = []; + var index = 0; + for (var f = 0; f < numberOfFrames; ++f) { + index = f * nItemPerFrame; + // calculate the size of a frame + var size = 0; + for (var i = 0; i < nItemPerFrame; ++i) { + size += pixItems[index + i].length; + } + // create new buffer + var newBuffer = new pixItems[0].constructor(size); + // fill new buffer + var fragOffset = 0; + for (var j = 0; j < nItemPerFrame; ++j) { + newBuffer.set(pixItems[index + j], fragOffset); + fragOffset += pixItems[index + j].length; + } + newPixItems[f] = newBuffer; + } + // store as pixel data + dataElement.value = newPixItems; + } + } + } }; +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + /** - * DicomParser class. + * Immutable tag. * * @class + * @param {string} group The tag group as '0x####'. + * @param {string} element The tag element as '0x####'. */ -dwv.dicom.DicomParser = function () { - /** - * The list of DICOM elements. - * - * @type {Array} - */ - this.dicomElements = {}; - - /** - * Default character set (optional). - * - * @private - * @type {string} - */ - var defaultCharacterSet; +dwv.dicom.Tag = function (group, element) { /** - * Get the default character set. + * Get the tag group. * - * @returns {string} The default character set. + * @returns {string} The tag group. */ - this.getDefaultCharacterSet = function () { - return defaultCharacterSet; + this.getGroup = function () { + return group; }; /** - * Set the default character set. - * param {String} The character set. + * Get the tag element. * - * @param {string} characterSet The input character set. + * @returns {string} The tag element. */ - this.setDefaultCharacterSet = function (characterSet) { - defaultCharacterSet = characterSet; + this.getElement = function () { + return element; }; -}; +}; // Tag class /** - * Get the raw DICOM data elements. + * Check for Tag equality. * - * @returns {object} The raw DICOM elements. + * @param {dwv.dicom.Tag} rhs The other tag to compare to. + * @returns {boolean} True if both tags are equal. */ -dwv.dicom.DicomParser.prototype.getRawDicomElements = function () { - return this.dicomElements; +dwv.dicom.Tag.prototype.equals = function (rhs) { + return rhs !== null && + this.getGroup() === rhs.getGroup() && + this.getElement() === rhs.getElement(); }; /** - * Get the DICOM data elements. + * Check for Tag equality. * - * @returns {object} The DICOM elements. + * @param {object} rhs The other tag to compare to provided as a simple object. + * @returns {boolean} True if both tags are equal. */ -dwv.dicom.DicomParser.prototype.getDicomElements = function () { - return new dwv.dicom.DicomElementsWrapper(this.dicomElements); +dwv.dicom.Tag.prototype.equals2 = function (rhs) { + if (rhs === null || + typeof rhs.group === 'undefined' || + typeof rhs.element === 'undefined') { + return false; + } + return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); }; /** - * Read a DICOM tag. + * Get the group-element key used to store DICOM elements. * - * @param {object} reader The raw data reader. - * @param {number} offset The offset where to start to read. - * @returns {object} An object containing the tags 'group', - * 'element' and 'name'. + * @returns {string} The key. */ -dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) { - // group - var group = reader.readHex(offset); - offset += Uint16Array.BYTES_PER_ELEMENT; - // element - var element = reader.readHex(offset); - offset += Uint16Array.BYTES_PER_ELEMENT; - // name - var name = dwv.dicom.getGroupElementKey(group, element); - // return - return { - group: group, - element: element, - name: name, - endOffset: offset - }; +dwv.dicom.Tag.prototype.getKey = function () { + return 'x' + this.getGroup().substr(2, 6) + this.getElement().substr(2, 6); }; /** - * Read an item data element. + * Get a simplified group-element key. * - * @param {object} reader The raw data reader. - * @param {number} offset The offset where to start to read. - * @param {boolean} implicit Is the DICOM VR implicit? - * @returns {object} The item data as a list of data elements. + * @returns {string} The key. */ -dwv.dicom.DicomParser.prototype.readItemDataElement = function ( - reader, offset, implicit) { - var itemData = {}; +dwv.dicom.Tag.prototype.getKey2 = function () { + return this.getGroup().substr(2, 6) + this.getElement().substr(2, 6); +}; - // read the first item - var item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; +/** + * Get the group name as defined in dwv.dicom.TagGroups. + * + * @returns {string} The name. + */ +dwv.dicom.Tag.prototype.getGroupName = function () { + return dwv.dicom.TagGroups[this.getGroup().substr(1)]; +}; - // exit if it is a sequence delimitation item - var isSeqDelim = (item.tag.name === 'xFFFEE0DD'); - if (isSeqDelim) { - return {data: itemData, - endOffset: item.endOffset, - isSeqDelim: isSeqDelim}; + +/** + * Split a group-element key used to store DICOM elements. + * + * @param {string} key The key in form "x00280102" as generated by tag::getKey. + * @returns {object} The DICOM tag. + */ +dwv.dicom.getTagFromKey = function (key) { + return new dwv.dicom.Tag(key.substr(1, 4), key.substr(5, 8)); +}; + +/** + * Does this tag have a VR. + * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. + * + * @returns {boolean} True if this tag has a VR. + */ +dwv.dicom.Tag.prototype.isWithVR = function () { + var element = this.getElement(); + return !(this.getGroup() === '0xFFFE' && + (element === '0xE000' || element === '0xE00D' || element === '0xE0DD') + ); +}; + +/** + * Is the tag group a private tag group ? + * see: http://dicom.nema.org/medical/dicom/2015a/output/html/part05.html#sect_7.8 + * + * @returns {boolean} True if the tag group is private, + * ie if its group is an odd number. + */ +dwv.dicom.Tag.prototype.isPrivate = function () { + var groupNumber = parseInt(this.getGroup().substr(2, 6), 10); + return groupNumber % 2 === 1; +}; + +/** + * Get the tag info from the dicom dictionary. + * + * @returns {Array} The info as [vr, multiplicity, name]. + */ +dwv.dicom.Tag.prototype.getInfoFromDictionary = function () { + var info = null; + if (typeof dwv.dicom.dictionary[this.getGroup()] !== 'undefined' && + typeof dwv.dicom.dictionary[this.getGroup()][this.getElement()] !== + 'undefined') { + info = dwv.dicom.dictionary[this.getGroup()][this.getElement()]; } + return info; +}; - // store it - itemData[item.tag.name] = item; +/** + * Get the tag Value Representation (VR) from the dicom dictionary. + * + * @returns {string} The VR. + */ +dwv.dicom.Tag.prototype.getVrFromDictionary = function () { + var vr = null; + var info = this.getInfoFromDictionary(); + if (info !== null) { + vr = info[0]; + } + return vr; +}; - if (item.vl !== 'u/l') { - // explicit VR items - // not empty - if (item.vl !== 0) { - // read until the end offset - var endOffset = offset; - offset -= item.vl; - while (offset < endOffset) { - item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; - itemData[item.tag.name] = item; - } - } - } else { - // implicit VR items - // read until the item delimitation item - var isItemDelim = false; - while (!isItemDelim) { - item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; - isItemDelim = (item.tag.name === 'xFFFEE00D'); - if (!isItemDelim) { - itemData[item.tag.name] = item; - } - } +/** + * Get the tag name from the dicom dictionary. + * + * @returns {string} The VR. + */ +dwv.dicom.Tag.prototype.getNameFromDictionary = function () { + var name = null; + var info = this.getInfoFromDictionary(); + if (info !== null) { + name = info[2]; } + return name; +}; - return { - data: itemData, - endOffset: offset, - isSeqDelim: false - }; +/** + * Get the TransferSyntaxUID Tag. + * + * @returns {object} The tag. + */ +dwv.dicom.getTransferSyntaxUIDTag = function () { + return new dwv.dicom.Tag('0x0002', '0x0010'); }; /** - * Read the pixel item data element. - * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}. + * Get the FileMetaInformationGroupLength Tag. * - * @param {object} reader The raw data reader. - * @param {number} offset The offset where to start to read. - * @param {boolean} implicit Is the DICOM VR implicit? - * @returns {Array} The item data as an array of data elements. + * @returns {object} The tag. */ -dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function ( - reader, offset, implicit) { - var itemData = []; +dwv.dicom.getFileMetaInformationGroupLengthTag = function () { + return new dwv.dicom.Tag('0x0002', '0x0000'); +}; - // first item: basic offset table - var item = this.readDataElement(reader, offset, implicit); - var offsetTableVl = item.vl; - offset = item.endOffset; +/** + * Is the input tag the FileMetaInformationGroupLength Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isFileMetaInformationGroupLengthTag = function (tag) { + return tag.equals(dwv.dicom.getFileMetaInformationGroupLengthTag()); +}; - // read until the sequence delimitation item - var isSeqDelim = false; - while (!isSeqDelim) { - item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; - isSeqDelim = (item.tag.name === 'xFFFEE0DD'); - if (!isSeqDelim) { - itemData.push(item.value); - } - } +/** + * Get the Item Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE000'); +}; - return { - data: itemData, - endOffset: offset, - offsetTableVl: offsetTableVl - }; +/** + * Is the input tag the Item Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isItemTag = function (tag) { + return tag.equals(dwv.dicom.getItemTag()); }; /** - * Read a DICOM data element. - * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}. + * Get the ItemDelimitationItem Tag. * - * @param {object} reader The raw data reader. - * @param {number} offset The offset where to start to read. - * @param {boolean} implicit Is the DICOM VR implicit? - * @returns {object} An object containing the element - * 'tag', 'vl', 'vr', 'data' and 'endOffset'. + * @returns {dwv.dicom.Tag} The tag. */ -dwv.dicom.DicomParser.prototype.readDataElement = function ( - reader, offset, implicit) { - // Tag: group, element - var tag = this.readTag(reader, offset); - offset = tag.endOffset; +dwv.dicom.getItemDelimitationItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE00D'); +}; - // Value Representation (VR) - var vr = null; - var is32bitVLVR = false; - if (dwv.dicom.isTagWithVR(tag.group, tag.element)) { - // implicit VR - if (implicit) { - vr = 'UN'; - var dict = dwv.dicom.dictionary; - if (typeof dict[tag.group] !== 'undefined' && - typeof dict[tag.group][tag.element] !== 'undefined') { - vr = dwv.dicom.dictionary[tag.group][tag.element][0]; - } - is32bitVLVR = true; - } else { - vr = reader.readString(offset, 2); - offset += 2 * Uint8Array.BYTES_PER_ELEMENT; - is32bitVLVR = dwv.dicom.is32bitVLVR(vr); - // reserved 2 bytes - if (is32bitVLVR) { - offset += 2 * Uint8Array.BYTES_PER_ELEMENT; +/** + * Is the input tag the ItemDelimitationItem Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isItemDelimitationItemTag = function (tag) { + return tag.equals(dwv.dicom.getItemDelimitationItemTag()); +}; + +/** + * Get the SequenceDelimitationItem Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getSequenceDelimitationItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE0DD'); +}; + +/** + * Is the input tag the SequenceDelimitationItem Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isSequenceDelimitationItemTag = function (tag) { + return tag.equals(dwv.dicom.getSequenceDelimitationItemTag()); +}; + +/** + * Get the PixelData Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getPixelDataTag = function () { + return new dwv.dicom.Tag('0x7FE0', '0x0010'); +}; + +/** + * Is the input tag the PixelData Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isPixelDataTag = function (tag) { + return tag.equals(dwv.dicom.getPixelDataTag()); +}; + +/** + * Get a tag from the dictionary using a tag string name. + * + * @param {string} tagName The tag string name. + * @returns {object} The tag object. + */ +dwv.dicom.getTagFromDictionary = function (tagName) { + var group = null; + var element = null; + var dict = dwv.dicom.dictionary; + var keys0 = Object.keys(dict); + var keys1 = null; + // label for nested loop break + outLabel: + // search through dictionary + for (var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) { + group = keys0[k0]; + keys1 = Object.keys(dict[group]); + for (var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) { + element = keys1[k1]; + if (dict[group][element][2] === tagName) { + break outLabel; } } - } else { - vr = 'UN'; - is32bitVLVR = true; - } - - // Value Length (VL) - var vl = 0; - if (is32bitVLVR) { - vl = reader.readUint32(offset); - offset += Uint32Array.BYTES_PER_ELEMENT; - } else { - vl = reader.readUint16(offset); - offset += Uint16Array.BYTES_PER_ELEMENT; } - - // check the value of VL - var vlString = vl; - if (vl === 0xffffffff) { - vlString = 'u/l'; - vl = 0; + var tag = null; + if (group !== null && element !== null) { + tag = new dwv.dicom.Tag(group, element); } + return tag; +}; - // treat private tag with unknown VR and zero VL as a sequence (see #799) - if (dwv.dicom.isPrivateGroup(tag.group) && vr === 'UN' && vl === 0) { - vr = 'SQ'; - } +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; - var startOffset = offset; +/** + * Get the dwv UID prefix. + * Issued by Medical Connections Ltd (www.medicalconnections.co.uk) + * on 25/10/2017. + * + * @returns {string} The dwv UID prefix. + */ +dwv.dicom.getDwvUIDPrefix = function () { + return '1.2.826.0.1.3680043.9.7278.1.'; +}; - // data - var data = null; - var isPixelData = (tag.name === 'x7FE00010'); - // pixel data sequence (implicit) - if (isPixelData && vlString === 'u/l') { - var pixItemData = this.readPixelItemDataElement(reader, offset, implicit); - offset = pixItemData.endOffset; - startOffset += pixItemData.offsetTableVl; - data = pixItemData.data; - } else if (isPixelData && - (vr === 'OB' || vr === 'OW' || vr === 'OF' || vr === 'ox')) { - // BitsAllocated - var bitsAllocated = 16; - if (typeof this.dicomElements.x00280100 !== 'undefined') { - bitsAllocated = this.dicomElements.x00280100.value[0]; - } else { - dwv.logger.warn('Reading DICOM pixel data with default bitsAllocated.'); - } - if (bitsAllocated === 8 && vr === 'OW') { - dwv.logger.warn( - 'Reading DICOM pixel data with vr=OW' + - ' and bitsAllocated=8 (should be 16).' - ); - } - if (bitsAllocated === 16 && vr === 'OB') { - dwv.logger.warn( - 'Reading DICOM pixel data with vr=OB' + - ' and bitsAllocated=16 (should be 8).' - ); - } - // PixelRepresentation 0->unsigned, 1->signed - var pixelRepresentation = 0; - if (typeof this.dicomElements.x00280103 !== 'undefined') { - pixelRepresentation = this.dicomElements.x00280103.value[0]; - } else { - dwv.logger.warn( - 'Reading DICOM pixel data with default pixelRepresentation.' - ); - } - // read - if (bitsAllocated === 1) { - data = reader.readBinaryArray(offset, vl); - } else if (bitsAllocated === 8) { - if (pixelRepresentation === 0) { - data = reader.readUint8Array(offset, vl); - } else { - data = reader.readInt8Array(offset, vl); - } - } else if (bitsAllocated === 16) { - if (pixelRepresentation === 0) { - data = reader.readUint16Array(offset, vl); - } else { - data = reader.readInt16Array(offset, vl); - } - } else if (bitsAllocated === 32) { - if (pixelRepresentation === 0) { - data = reader.readUint32Array(offset, vl); - } else { - data = reader.readInt32Array(offset, vl); - } - } else if (bitsAllocated === 64) { - if (pixelRepresentation === 0) { - data = reader.readUint64Array(offset, vl); - } else { - data = reader.readInt64Array(offset, vl); - } - } else { - throw new Error('Unsupported bits allocated: ' + bitsAllocated); - } - offset += vl; - } else if (vr === 'OB') { - data = reader.readUint8Array(offset, vl); - offset += vl; - } else if (vr === 'OW') { - data = reader.readUint16Array(offset, vl); - offset += vl; - } else if (vr === 'OF') { - data = reader.readUint32Array(offset, vl); - offset += vl; - } else if (vr === 'OD') { - data = reader.readUint64Array(offset, vl); - offset += vl; - } else if (vr === 'US') { - data = reader.readUint16Array(offset, vl); - offset += vl; - } else if (vr === 'UL') { - data = reader.readUint32Array(offset, vl); - offset += vl; - } else if (vr === 'SS') { - data = reader.readInt16Array(offset, vl); - offset += vl; - } else if (vr === 'SL') { - data = reader.readInt32Array(offset, vl); - offset += vl; - } else if (vr === 'FL') { - data = reader.readFloat32Array(offset, vl); - offset += vl; - } else if (vr === 'FD') { - data = reader.readFloat64Array(offset, vl); - offset += vl; - } else if (vr === 'xs') { - // PixelRepresentation 0->unsigned, 1->signed - var pixelRep = 0; - if (typeof this.dicomElements.x00280103 !== 'undefined') { - pixelRep = this.dicomElements.x00280103.value[0]; - } else { - dwv.logger.warn( - 'Reading DICOM pixel data with default pixelRepresentation.'); - } - // read - if (pixelRep === 0) { - data = reader.readUint16Array(offset, vl); - } else { - data = reader.readInt16Array(offset, vl); - } - offset += vl; - } else if (vr === 'AT') { - // attribute - var raw = reader.readUint16Array(offset, vl); - offset += vl; - data = []; - for (var i = 0, leni = raw.length; i < leni; i += 2) { - var stri = raw[i].toString(16); - var stri1 = raw[i + 1].toString(16); - var str = '('; - str += '0000'.substr(0, 4 - stri.length) + stri.toUpperCase(); - str += ','; - str += '0000'.substr(0, 4 - stri1.length) + stri1.toUpperCase(); - str += ')'; - data.push(str); - } - } else if (vr === 'UN') { - // not available - data = reader.readUint8Array(offset, vl); - offset += vl; - } else if (vr === 'SQ') { - // sequence - data = []; - var itemData; - // explicit VR sequence - if (vlString !== 'u/l') { - // not empty - if (vl !== 0) { - var sqEndOffset = offset + vl; - while (offset < sqEndOffset) { - itemData = this.readItemDataElement(reader, offset, implicit); - data.push(itemData.data); - offset = itemData.endOffset; - } - } - } else { - // implicit VR sequence - // read until the sequence delimitation item - var isSeqDelim = false; - while (!isSeqDelim) { - itemData = this.readItemDataElement(reader, offset, implicit); - isSeqDelim = itemData.isSeqDelim; - offset = itemData.endOffset; - // do not store the delimitation item - if (!isSeqDelim) { - data.push(itemData.data); - } - } +/** + * Get a UID for a DICOM tag. + * + * @see http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_9.html + * @see http://dicomiseasy.blogspot.com/2011/12/chapter-4-dicom-objects-in-chapter-3.html + * @see https://stackoverflow.com/questions/46304306/how-to-generate-unique-dicom-uid + * @param {string} tagName The input tag. + * @returns {string} The corresponding UID. + */ +dwv.dicom.getUID = function (tagName) { + var uid = dwv.dicom.getDwvUIDPrefix(); + if (tagName === 'ImplementationClassUID') { + uid += dwv.getVersion(); + } else if (tagName === 'SOPInstanceUID') { + for (var i = 0; i < tagName.length; ++i) { + uid += tagName.charCodeAt(i); } + // add date (only numbers) + uid += '.' + (new Date()).toISOString().replace(/\D/g, ''); } else { - // raw - if (vr === 'SH' || vr === 'LO' || vr === 'ST' || - vr === 'PN' || vr === 'LT' || vr === 'UT') { - data = reader.readSpecialString(offset, vl); - } else { - data = reader.readString(offset, vl); - } - offset += vl; - data = data.split('\\'); + throw new Error('Don\'t know how to generate a UID for the tag ' + tagName); } + return uid; +}; - // return - return { - tag: tag, - vr: vr, - vl: vlString, - value: data, - startOffset: startOffset, - endOffset: offset - }; +/** + * Return true if the input number is even. + * + * @param {number} number The number to check. + * @returns {boolean} True is the number is even. + */ +dwv.dicom.isEven = function (number) { + return number % 2 === 0; }; /** - * Parse the complete DICOM file (given as input to the class). - * Fills in the member object 'dicomElements'. + * Is the input VR a non string VR. * - * @param {object} buffer The input array buffer. + * @param {string} vr The element VR. + * @returns {boolean} True if the VR is a non string one. */ -dwv.dicom.DicomParser.prototype.parse = function (buffer) { - var offset = 0; - var implicit = false; - var syntax = ''; - var dataElement = null; - // default readers - var metaReader = new dwv.dicom.DataReader(buffer); - var dataReader = new dwv.dicom.DataReader(buffer); +dwv.dicom.isNonStringVr = function (vr) { + return vr === 'UN' || vr === 'OB' || vr === 'OW' || + vr === 'OF' || vr === 'OD' || vr === 'US' || vr === 'SS' || + vr === 'UL' || vr === 'SL' || vr === 'FL' || vr === 'FD' || + vr === 'SQ' || vr === 'AT'; +}; - // 128 -> 132: magic word - offset = 128; - var magicword = metaReader.readString(offset, 4); - offset += 4 * Uint8Array.BYTES_PER_ELEMENT; - if (magicword === 'DICM') { - // 0x0002, 0x0000: FileMetaInformationGroupLength - dataElement = this.readDataElement(metaReader, offset, false); - offset = dataElement.endOffset; - // store the data element - this.dicomElements[dataElement.tag.name] = dataElement; - // get meta length - var metaLength = parseInt(dataElement.value[0], 10); +/** + * Is the input VR a string VR. + * + * @param {string} vr The element VR. + * @returns {boolean} True if the VR is a string one. + */ +dwv.dicom.isStringVr = function (vr) { + return !dwv.dicom.isNonStringVr(vr); +}; - // meta elements - var metaEnd = offset + metaLength; - while (offset < metaEnd) { - // get the data element - dataElement = this.readDataElement(metaReader, offset, false); - offset = dataElement.endOffset; - // store the data element - this.dicomElements[dataElement.tag.name] = dataElement; - } - } else { - // no metadata: attempt to detect transfer syntax - // see https://github.com/ivmartel/dwv/issues/188 - // (Allow to load DICOM with no DICM preamble) for more details - var oEightGroupBigEndian = '0x0800'; - var oEightGroupLittleEndian = '0x0008'; - // read first element - dataElement = this.readDataElement(dataReader, 0, implicit); - // check that group is 0x0008 - if ((dataElement.tag.group !== oEightGroupBigEndian) && - (dataElement.tag.group !== oEightGroupLittleEndian)) { - throw new Error( - 'Not a valid DICOM file (no magic DICM word found' + - 'and first element not in 0x0008 group)' - ); - } - // reasonable assumption: 2 uppercase characters => explicit vr - var vr0 = dataElement.vr.charCodeAt(0); - var vr1 = dataElement.vr.charCodeAt(1); - implicit = (vr0 >= 65 && vr0 <= 90 && vr1 >= 65 && vr1 <= 90) - ? false : true; - // guess transfer syntax - if (dataElement.tag.group === oEightGroupLittleEndian) { - if (implicit) { - // ImplicitVRLittleEndian - syntax = '1.2.840.10008.1.2'; - } else { - // ExplicitVRLittleEndian - syntax = '1.2.840.10008.1.2.1'; - } +/** + * Is the input VR a VR that could need padding. + * + * @param {string} vr The element VR. + * @returns {boolean} True if the VR needs padding. + */ +dwv.dicom.isVrToPad = function (vr) { + return dwv.dicom.isStringVr(vr) || vr === 'OB'; +}; + +/** + * Get the VR specific padding value. + * + * @param {string} vr The element VR. + * @returns {boolean} The value used to pad. + */ +dwv.dicom.getVrPad = function (vr) { + var pad = 0; + if (dwv.dicom.isStringVr(vr)) { + if (vr === 'UI') { + pad = '\0'; } else { - if (implicit) { - // ImplicitVRBigEndian: impossible - throw new Error( - 'Not a valid DICOM file (no magic DICM word found' + - 'and implicit VR big endian detected)' - ); - } else { - // ExplicitVRBigEndian - syntax = '1.2.840.10008.1.2.2'; - } + pad = ' '; } - // set transfer syntax data element - dataElement.tag.group = '0x0002'; - dataElement.tag.element = '0x0010'; - dataElement.tag.name = 'x00020010'; - dataElement.tag.endOffset = 4; - dataElement.vr = 'UI'; - dataElement.value = [syntax + ' ']; // even length - dataElement.vl = dataElement.value[0].length; - dataElement.endOffset = dataElement.startOffset + dataElement.vl; - // store it - this.dicomElements[dataElement.tag.name] = dataElement; - - // reset offset - offset = 0; } + return pad; +}; - // check the TransferSyntaxUID (has to be there!) - if (typeof this.dicomElements.x00020010 === 'undefined') { - throw new Error('Not a valid DICOM file (no TransferSyntaxUID found)'); +/** + * Pad an input value according to its VR. + * see http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html + * + * @param {object} element The DICOM element to get the VR from. + * @param {object} value The value to pad. + * @returns {string} The padded value. + */ +dwv.dicom.padElementValue = function (element, value) { + if (typeof value !== 'undefined' && typeof value.length !== 'undefined') { + if (dwv.dicom.isVrToPad(element.vr) && !dwv.dicom.isEven(value.length)) { + if (value instanceof Array) { + value[value.length - 1] += dwv.dicom.getVrPad(element.vr); + } else { + value += dwv.dicom.getVrPad(element.vr); + } + } } - syntax = dwv.dicom.cleanString(this.dicomElements.x00020010.value[0]); - - // check support - if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) { - throw new Error('Unsupported DICOM transfer syntax: \'' + syntax + - '\' (' + dwv.dicom.getTransferSyntaxName(syntax) + ')'); - } - - // Implicit VR - if (dwv.dicom.isImplicitTransferSyntax(syntax)) { - implicit = true; - } - - // Big Endian - if (dwv.dicom.isBigEndianTransferSyntax(syntax)) { - dataReader = new dwv.dicom.DataReader(buffer, false); - } - - // default character set - if (typeof this.getDefaultCharacterSet() !== 'undefined') { - dataReader.setUtfLabel(this.getDefaultCharacterSet()); - } - - // DICOM data elements - while (offset < buffer.byteLength) { - // get the data element - dataElement = this.readDataElement(dataReader, offset, implicit); - // check character set - if (dataElement.tag.name === 'x00080005') { - var charSetTerm; - if (dataElement.value.length === 1) { - charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); - } else { - charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); - dwv.logger.warn('Unsupported character set with code extensions: \'' + - charSetTerm + '\'.'); - } - dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); - } - // increment offset - offset = dataElement.endOffset; - // store the data element - if (typeof this.dicomElements[dataElement.tag.name] === 'undefined') { - this.dicomElements[dataElement.tag.name] = dataElement; - } else { - dwv.logger.warn('Not saving duplicate tag: ' + dataElement.tag.name); - } - } - - // safety check... - if (buffer.byteLength !== offset) { - dwv.logger.warn('Did not reach the end of the buffer: ' + - offset + ' != ' + buffer.byteLength); - } - - // pixel buffer - if (typeof this.dicomElements.x7FE00010 !== 'undefined') { - - var numberOfFrames = 1; - if (typeof this.dicomElements.x00280008 !== 'undefined') { - numberOfFrames = dwv.dicom.cleanString( - this.dicomElements.x00280008.value[0]); - } - - if (this.dicomElements.x7FE00010.vl !== 'u/l') { - // compressed should be encapsulated... - if (dwv.dicom.isJpeg2000TransferSyntax(syntax) || - dwv.dicom.isJpegBaselineTransferSyntax(syntax) || - dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { - dwv.logger.warn('Compressed but no items...'); - } - - // calculate the slice size - var pixData = this.dicomElements.x7FE00010.value; - if (pixData && typeof pixData !== 'undefined' && - pixData.length !== 0) { - if (typeof this.dicomElements.x00280010 === 'undefined') { - throw new Error('Missing image number of rows.'); - } - if (typeof this.dicomElements.x00280011 === 'undefined') { - throw new Error('Missing image number of columns.'); - } - if (typeof this.dicomElements.x00280002 === 'undefined') { - throw new Error('Missing image samples per pixel.'); - } - var columns = this.dicomElements.x00280011.value[0]; - var rows = this.dicomElements.x00280010.value[0]; - var samplesPerPixel = this.dicomElements.x00280002.value[0]; - var sliceSize = columns * rows * samplesPerPixel; - // slice data in an array of frames - var newPixData = []; - var frameOffset = 0; - for (var g = 0; g < numberOfFrames; ++g) { - newPixData[g] = pixData.slice(frameOffset, frameOffset + sliceSize); - frameOffset += sliceSize; - } - // store as pixel data - this.dicomElements.x7FE00010.value = newPixData; - } else { - dwv.logger.info('Empty pixel data.'); - } - } else { - // handle fragmented pixel buffer - // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html - // (third note, "Depending on the transfer syntax...") - var pixItems = this.dicomElements.x7FE00010.value; - if (pixItems.length > 1 && pixItems.length > numberOfFrames) { - - // concatenate pixel data items - // concat does not work on typed arrays - //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); - // manual concat... - var nItemPerFrame = pixItems.length / numberOfFrames; - var newPixItems = []; - var index = 0; - for (var f = 0; f < numberOfFrames; ++f) { - index = f * nItemPerFrame; - // calculate the size of a frame - var size = 0; - for (var i = 0; i < nItemPerFrame; ++i) { - size += pixItems[index + i].length; - } - // create new buffer - var newBuffer = new pixItems[0].constructor(size); - // fill new buffer - var fragOffset = 0; - for (var j = 0; j < nItemPerFrame; ++j) { - newBuffer.set(pixItems[index + j], fragOffset); - fragOffset += pixItems[index + j].length; - } - newPixItems[f] = newBuffer; - } - // store as pixel data - this.dicomElements.x7FE00010.value = newPixItems; - } - } - } -}; - -// namespaces -var dwv = dwv || {}; -dwv.dicom = dwv.dicom || {}; - -/** - * Get the dwv UID prefix. - * Issued by Medical Connections Ltd (www.medicalconnections.co.uk) - * on 25/10/2017. - * - * @returns {string} The dwv UID prefix. - */ -dwv.dicom.getDwvUIDPrefix = function () { - return '1.2.826.0.1.3680043.9.7278.1.'; -}; + return value; +}; /** - * Get a UID for a DICOM tag. + * Is this element an implicit length sequence? * - * @see http://dicom.nema.org/dicom/2013/output/chtml/part05/chapter_9.html - * @see http://dicomiseasy.blogspot.com/2011/12/chapter-4-dicom-objects-in-chapter-3.html - * @see https://stackoverflow.com/questions/46304306/how-to-generate-unique-dicom-uid - * @param {string} tagName The input tag. - * @returns {string} The corresponding UID. + * @param {object} element The element to check. + * @returns {boolean} True if it is. */ -dwv.dicom.getUID = function (tagName) { - var uid = dwv.dicom.getDwvUIDPrefix(); - if (tagName === 'ImplementationClassUID') { - uid += dwv.getVersion(); - } else if (tagName === 'SOPInstanceUID') { - for (var i = 0; i < tagName.length; ++i) { - uid += tagName.charCodeAt(i); - } - // add date (only numbers) - uid += '.' + (new Date()).toISOString().replace(/\D/g, ''); - } else { - throw new Error('Don\'t know how to generate a UID for the tag ' + tagName); - } - return uid; +dwv.dicom.isImplicitLengthSequence = function (element) { + // sequence with no length + return (element.vr === 'SQ') && + (element.vl === 'u/l'); }; /** - * Return true if the input number is even. + * Is this element an implicit length item? * - * @param {number} number The number to check. - * @returns {boolean} True is the number is even. + * @param {object} element The element to check. + * @returns {boolean} True if it is. */ -dwv.dicom.isEven = function (number) { - return number % 2 === 0; +dwv.dicom.isImplicitLengthItem = function (element) { + // item with no length + return (element.tag.name === 'xFFFEE000') && + (element.vl === 'u/l'); }; /** - * Is the input VR a non string VR. + * Is this element an implicit length pixel data? * - * @param {string} vr The element VR. - * @returns {boolean} True if the VR is a non string one. + * @param {object} element The element to check. + * @returns {boolean} True if it is. */ -dwv.dicom.isNonStringVr = function (vr) { - return vr === 'UN' || vr === 'OB' || vr === 'OW' || - vr === 'OF' || vr === 'OD' || vr === 'US' || vr === 'SS' || - vr === 'UL' || vr === 'SL' || vr === 'FL' || vr === 'FD' || - vr === 'SQ' || vr === 'AT'; +dwv.dicom.isImplicitLengthPixels = function (element) { + // pixel data with no length + return (element.tag.name === 'x7FE00010') && + (element.vl === 'u/l'); }; /** - * Is the input VR a string VR. + * Helper method to flatten an array of typed arrays to 2D typed array * - * @param {string} vr The element VR. - * @returns {boolean} True if the VR is a string one. + * @param {Array} initialArray array of typed arrays + * @returns {object} a typed array containing all values */ -dwv.dicom.isStringVr = function (vr) { - return !dwv.dicom.isNonStringVr(vr); -}; +dwv.dicom.flattenArrayOfTypedArrays = function (initialArray) { + var initialArrayLength = initialArray.length; + var arrayLength = initialArray[0].length; + // If this is not a array of arrays, just return the initial one: + if (typeof arrayLength === 'undefined') { + return initialArray; + } -/** - * Is the input VR a VR that could need padding. - * - * @param {string} vr The element VR. - * @returns {boolean} True if the VR needs padding. - */ -dwv.dicom.isVrToPad = function (vr) { - return dwv.dicom.isStringVr(vr) || vr === 'OB'; -}; + var flattenendArrayLength = initialArrayLength * arrayLength; -/** - * Get the VR specific padding value. - * - * @param {string} vr The element VR. - * @returns {boolean} The value used to pad. - */ -dwv.dicom.getVrPad = function (vr) { - var pad = 0; - if (dwv.dicom.isStringVr(vr)) { - if (vr === 'UI') { - pad = '\0'; - } else { - pad = ' '; - } - } - return pad; -}; + var flattenedArray = new initialArray[0].constructor(flattenendArrayLength); -/** - * Pad an input value according to its VR. - * see http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html - * - * @param {object} element The DICOM element to get the VR from. - * @param {object} value The value to pad. - * @returns {string} The padded value. - */ -dwv.dicom.padElementValue = function (element, value) { - if (typeof value !== 'undefined' && typeof value.length !== 'undefined') { - if (dwv.dicom.isVrToPad(element.vr) && !dwv.dicom.isEven(value.length)) { - if (value instanceof Array) { - value[value.length - 1] += dwv.dicom.getVrPad(element.vr); - } else { - value += dwv.dicom.getVrPad(element.vr); - } - } + for (var i = 0; i < initialArrayLength; i++) { + var indexFlattenedArray = i * arrayLength; + flattenedArray.set(initialArray[i], indexFlattenedArray); } - return value; + return flattenedArray; }; /** - * Data writer. + * DICOM writer. * * Example usage: * var parser = new dwv.dicom.DicomParser(); @@ -5985,447 +6040,253 @@ dwv.dicom.padElementValue = function (element, value) { * element.download = "anonym.dcm"; * * @class - * @param {Array} buffer The input array buffer. - * @param {boolean} isLittleEndian Flag to tell if the data is - * little or big endian. */ -dwv.dicom.DataWriter = function (buffer, isLittleEndian) { - // Set endian flag if not defined. - if (typeof isLittleEndian === 'undefined') { - isLittleEndian = true; - } - - // private DataView - var view = new DataView(buffer); +dwv.dicom.DicomWriter = function () { // flag to use VR=UN for private sequences, default to false // (mainly used in tests) this.useUnVrForPrivateSq = false; - /** - * Write Uint8 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint8 = function (byteOffset, value) { - view.setUint8(byteOffset, value); - return byteOffset + Uint8Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Int8 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt8 = function (byteOffset, value) { - view.setInt8(byteOffset, value); - return byteOffset + Int8Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Uint16 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint16 = function (byteOffset, value) { - view.setUint16(byteOffset, value, isLittleEndian); - return byteOffset + Uint16Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Int16 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt16 = function (byteOffset, value) { - view.setInt16(byteOffset, value, isLittleEndian); - return byteOffset + Int16Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Uint32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint32 = function (byteOffset, value) { - view.setUint32(byteOffset, value, isLittleEndian); - return byteOffset + Uint32Array.BYTES_PER_ELEMENT; + // possible tag actions + var actions = { + copy: function (item) { + return item; + }, + remove: function () { + return null; + }, + clear: function (item) { + item.value[0] = ''; + item.vl = 0; + item.endOffset = item.startOffset; + return item; + }, + replace: function (item, value) { + var paddedValue = dwv.dicom.padElementValue(item, value); + item.value[0] = paddedValue; + item.vl = paddedValue.length; + item.endOffset = item.startOffset + paddedValue.length; + return item; + } }; - /** - * Write Int32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt32 = function (byteOffset, value) { - view.setInt32(byteOffset, value, isLittleEndian); - return byteOffset + Int32Array.BYTES_PER_ELEMENT; + // default rules: just copy + var defaultRules = { + default: {action: 'copy', value: null} }; /** - * Write Float32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. + * Public (modifiable) rules. + * Set of objects as: + * name : { action: 'actionName', value: 'optionalValue } + * The names are either 'default', tagName or groupName. + * Each DICOM element will be checked to see if a rule is applicable. + * First checked by tagName and then by groupName, + * if nothing is found the default rule is applied. */ - this.writeFloat32 = function (byteOffset, value) { - view.setFloat32(byteOffset, value, isLittleEndian); - return byteOffset + Float32Array.BYTES_PER_ELEMENT; - }; + this.rules = defaultRules; /** - * Write Float64 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. + * Example anonymisation rules. */ - this.writeFloat64 = function (byteOffset, value) { - view.setFloat64(byteOffset, value, isLittleEndian); - return byteOffset + Float64Array.BYTES_PER_ELEMENT; + this.anonymisationRules = { + default: {action: 'remove', value: null}, + PatientName: {action: 'replace', value: 'Anonymized'}, // tag + 'Meta Element': {action: 'copy', value: null}, // group 'x0002' + Acquisition: {action: 'copy', value: null}, // group 'x0018' + 'Image Presentation': {action: 'copy', value: null}, // group 'x0028' + Procedure: {action: 'copy', value: null}, // group 'x0040' + 'Pixel Data': {action: 'copy', value: null} // group 'x7fe0' }; /** - * Write string data as hexadecimal. + * Get the element to write according to the class rules. + * Priority order: tagName, groupName, default. * - * @param {number} byteOffset The offset to start writing from. - * @param {number} str The padded hexadecimal string to write ('0x####'). - * @returns {number} The new offset position. + * @param {object} element The element to check + * @returns {object} The element to write, can be null. */ - this.writeHex = function (byteOffset, str) { - // remove first two chars and parse - var value = parseInt(str.substr(2), 16); - view.setUint16(byteOffset, value, isLittleEndian); - return byteOffset + Uint16Array.BYTES_PER_ELEMENT; - }; + this.getElementToWrite = function (element) { + // get group and tag string name + var tag = new dwv.dicom.Tag(element.tag.group, element.tag.element); + var groupName = tag.getGroupName(); + var tagName = tag.getNameFromDictionary(); - /** - * Write string data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} str The data to write. - * @returns {number} The new offset position. - */ - this.writeString = function (byteOffset, str) { - for (var i = 0, len = str.length; i < len; ++i) { - view.setUint8(byteOffset, str.charCodeAt(i)); - byteOffset += Uint8Array.BYTES_PER_ELEMENT; + // apply rules: + var rule; + if (typeof this.rules[element.tag.name] !== 'undefined') { + // 1. tag itself + rule = this.rules[element.tag.name]; + } else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') { + // 2. tag name + rule = this.rules[tagName]; + } else if (typeof this.rules[groupName] !== 'undefined') { + // 3. group name + rule = this.rules[groupName]; + } else { + // 4. default + rule = this.rules['default']; } - return byteOffset; + // apply action on element and return + return actions[rule.action](element, rule.value); }; - }; /** - * Write a boolean array as binary. + * Write a list of items. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. + * @param {Array} items The list of items to write. + * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeBinaryArray = function (byteOffset, array) { - if (array.length % 8 !== 0) { - throw new Error('Cannot write boolean array as binary.'); - } - var byte = null; - var val = null; - for (var i = 0, len = array.length; i < len; i += 8) { - byte = 0; - for (var j = 0; j < 8; ++j) { - val = array[i + j] === 0 ? 0 : 1; - byte += val << j; +dwv.dicom.DicomWriter.prototype.writeDataElementItems = function ( + writer, byteOffset, items, isImplicit) { + var item = null; + for (var i = 0; i < items.length; ++i) { + item = items[i]; + var itemKeys = Object.keys(item); + if (itemKeys.length === 0) { + continue; + } + // item element (create new to not modify original) + var implicitLength = item.xFFFEE000.vl === 'u/l'; + var itemElement = { + tag: item.xFFFEE000.tag, + vr: item.xFFFEE000.vr, + vl: implicitLength ? 0xffffffff : item.xFFFEE000.vl, + value: [] + }; + byteOffset = this.writeDataElement( + writer, itemElement, byteOffset, isImplicit); + // write rest + for (var m = 0; m < itemKeys.length; ++m) { + if (itemKeys[m] !== 'xFFFEE000' && itemKeys[m] !== 'xFFFEE00D') { + byteOffset = this.writeDataElement( + writer, item[itemKeys[m]], byteOffset, isImplicit); + } + } + // item delimitation + if (implicitLength) { + var itemDelimElement = { + tag: { + group: '0xFFFE', + element: '0xE00D', + name: 'ItemDelimitationItem' + }, + vr: 'NONE', + vl: 0, + value: [] + }; + byteOffset = this.writeDataElement( + writer, itemDelimElement, byteOffset, isImplicit); } - byteOffset = this.writeUint8(byteOffset, byte); } + + // return new offset return byteOffset; }; /** - * Write Uint8 array. + * Write data with a specific Value Representation (VR). * + * @param {dwv.dicom.DataWriter} writer The raw data writer. + * @param {string} vr The data Value Representation (VR). + * @param {string} vl The data Value Length (VL). * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. + * @param {Array} value The array to write. + * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeUint8Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint8(byteOffset, array[i]); +dwv.dicom.DicomWriter.prototype.writeDataElementValue = function ( + writer, vr, vl, byteOffset, value, isImplicit) { + // first check input type to know how to write + if (value instanceof Uint8Array) { + // binary data has been expanded 8 times at read + if (value.length === 8 * vl) { + byteOffset = writer.writeBinaryArray(byteOffset, value); + } else { + byteOffset = writer.writeUint8Array(byteOffset, value); + } + } else if (value instanceof Int8Array) { + byteOffset = writer.writeInt8Array(byteOffset, value); + } else if (value instanceof Uint16Array) { + byteOffset = writer.writeUint16Array(byteOffset, value); + } else if (value instanceof Int16Array) { + byteOffset = writer.writeInt16Array(byteOffset, value); + } else if (value instanceof Uint32Array) { + byteOffset = writer.writeUint32Array(byteOffset, value); + } else if (value instanceof Int32Array) { + byteOffset = writer.writeInt32Array(byteOffset, value); + } else { + // switch according to VR if input type is undefined + if (vr === 'UN') { + byteOffset = writer.writeUint8Array(byteOffset, value); + } else if (vr === 'OB') { + byteOffset = writer.writeInt8Array(byteOffset, value); + } else if (vr === 'OW') { + byteOffset = writer.writeInt16Array(byteOffset, value); + } else if (vr === 'OF') { + byteOffset = writer.writeInt32Array(byteOffset, value); + } else if (vr === 'OD') { + byteOffset = writer.writeInt64Array(byteOffset, value); + } else if (vr === 'US') { + byteOffset = writer.writeUint16Array(byteOffset, value); + } else if (vr === 'SS') { + byteOffset = writer.writeInt16Array(byteOffset, value); + } else if (vr === 'UL') { + byteOffset = writer.writeUint32Array(byteOffset, value); + } else if (vr === 'SL') { + byteOffset = writer.writeInt32Array(byteOffset, value); + } else if (vr === 'FL') { + byteOffset = writer.writeFloat32Array(byteOffset, value); + } else if (vr === 'FD') { + byteOffset = writer.writeFloat64Array(byteOffset, value); + } else if (vr === 'SQ') { + byteOffset = this.writeDataElementItems( + writer, byteOffset, value, isImplicit); + } else if (vr === 'AT') { + for (var i = 0; i < value.length; ++i) { + var hexString = value[i] + ''; + var hexString1 = hexString.substring(1, 5); + var hexString2 = hexString.substring(6, 10); + var dec1 = parseInt(hexString1, 16); + var dec2 = parseInt(hexString2, 16); + var atValue = new Uint16Array([dec1, dec2]); + byteOffset = writer.writeUint16Array(byteOffset, atValue); + } + } else { + // join if array + if (Array.isArray(value)) { + value = value.join('\\'); + } + // write + if (vr === 'SH' || vr === 'LO' || vr === 'ST' || + vr === 'PN' || vr === 'LT' || vr === 'UT') { + byteOffset = writer.writeSpecialString(byteOffset, value); + } else { + byteOffset = writer.writeString(byteOffset, value); + } + } } + // return new offset return byteOffset; }; /** - * Write Int8 array. + * Write a pixel data element. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. + * @param {string} vr The data Value Representation (VR). + * @param {string} vl The data Value Length (VL). * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. + * @param {Array} value The array to write. + * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeInt8Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt8(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Uint16 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeUint16Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint16(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Int16 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeInt16Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt16(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Uint32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeUint32Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Int32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeInt32Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Float32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeFloat32Array = function ( - byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeFloat32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Float64 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeFloat64Array = function ( - byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeFloat64(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write string array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeStringArray = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - // separator - if (i !== 0) { - byteOffset = this.writeString(byteOffset, '\\'); - } - // value - byteOffset = this.writeString(byteOffset, array[i].toString()); - } - return byteOffset; -}; - -/** - * Write a list of items. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} items The list of items to write. - * @param {boolean} isImplicit Is the DICOM VR implicit? - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( - byteOffset, items, isImplicit) { - var item = null; - for (var i = 0; i < items.length; ++i) { - item = items[i]; - var itemKeys = Object.keys(item); - if (itemKeys.length === 0) { - continue; - } - // item element (create new to not modify original) - var implicitLength = item.xFFFEE000.vl === 'u/l'; - var itemElement = { - tag: item.xFFFEE000.tag, - vr: item.xFFFEE000.vr, - vl: implicitLength ? 0xffffffff : item.xFFFEE000.vl, - value: [] - }; - byteOffset = this.writeDataElement(itemElement, byteOffset, isImplicit); - // write rest - for (var m = 0; m < itemKeys.length; ++m) { - if (itemKeys[m] !== 'xFFFEE000' && itemKeys[m] !== 'xFFFEE00D') { - byteOffset = this.writeDataElement( - item[itemKeys[m]], byteOffset, isImplicit); - } - } - // item delimitation - if (implicitLength) { - var itemDelimElement = { - tag: { - group: '0xFFFE', - element: '0xE00D', - name: 'ItemDelimitationItem' - }, - vr: 'NONE', - vl: 0, - value: [] - }; - byteOffset = this.writeDataElement( - itemDelimElement, byteOffset, isImplicit); - } - } - - // return new offset - return byteOffset; -}; - -/** - * Write data with a specific Value Representation (VR). - * - * @param {string} vr The data Value Representation (VR). - * @param {string} vl The data Value Length (VL). - * @param {number} byteOffset The offset to start writing from. - * @param {Array} value The array to write. - * @param {boolean} isImplicit Is the DICOM VR implicit? - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeDataElementValue = function ( - vr, vl, byteOffset, value, isImplicit) { - // first check input type to know how to write - if (value instanceof Uint8Array) { - // binary data has been expanded 8 times at read - if (value.length === 8 * vl) { - byteOffset = this.writeBinaryArray(byteOffset, value); - } else { - byteOffset = this.writeUint8Array(byteOffset, value); - } - } else if (value instanceof Int8Array) { - byteOffset = this.writeInt8Array(byteOffset, value); - } else if (value instanceof Uint16Array) { - byteOffset = this.writeUint16Array(byteOffset, value); - } else if (value instanceof Int16Array) { - byteOffset = this.writeInt16Array(byteOffset, value); - } else if (value instanceof Uint32Array) { - byteOffset = this.writeUint32Array(byteOffset, value); - } else if (value instanceof Int32Array) { - byteOffset = this.writeInt32Array(byteOffset, value); - } else { - // switch according to VR if input type is undefined - if (vr === 'UN') { - byteOffset = this.writeUint8Array(byteOffset, value); - } else if (vr === 'OB') { - byteOffset = this.writeInt8Array(byteOffset, value); - } else if (vr === 'OW') { - byteOffset = this.writeInt16Array(byteOffset, value); - } else if (vr === 'OF') { - byteOffset = this.writeInt32Array(byteOffset, value); - } else if (vr === 'OD') { - byteOffset = this.writeInt64Array(byteOffset, value); - } else if (vr === 'US') { - byteOffset = this.writeUint16Array(byteOffset, value); - } else if (vr === 'SS') { - byteOffset = this.writeInt16Array(byteOffset, value); - } else if (vr === 'UL') { - byteOffset = this.writeUint32Array(byteOffset, value); - } else if (vr === 'SL') { - byteOffset = this.writeInt32Array(byteOffset, value); - } else if (vr === 'FL') { - byteOffset = this.writeFloat32Array(byteOffset, value); - } else if (vr === 'FD') { - byteOffset = this.writeFloat64Array(byteOffset, value); - } else if (vr === 'SQ') { - byteOffset = this.writeDataElementItems(byteOffset, value, isImplicit); - } else if (vr === 'AT') { - for (var i = 0; i < value.length; ++i) { - var hexString = value[i] + ''; - var hexString1 = hexString.substring(1, 5); - var hexString2 = hexString.substring(6, 10); - var dec1 = parseInt(hexString1, 16); - var dec2 = parseInt(hexString2, 16); - var atValue = new Uint16Array([dec1, dec2]); - byteOffset = this.writeUint16Array(byteOffset, atValue); - } - } else { - byteOffset = this.writeStringArray(byteOffset, value); - } - } - // return new offset - return byteOffset; -}; - -/** - * Write a pixel data element. - * - * @param {string} vr The data Value Representation (VR). - * @param {string} vl The data Value Length (VL). - * @param {number} byteOffset The offset to start writing from. - * @param {Array} value The array to write. - * @param {boolean} isImplicit Is the DICOM VR implicit? - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( - vr, vl, byteOffset, value, isImplicit) { +dwv.dicom.DicomWriter.prototype.writePixelDataElementValue = function ( + writer, vr, vl, byteOffset, value, isImplicit) { // explicit length if (vl !== 'u/l') { var finalValue = value[0]; @@ -6435,7 +6296,7 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( } // write byteOffset = this.writeDataElementValue( - vr, vl, byteOffset, finalValue, isImplicit); + writer, vr, vl, byteOffset, finalValue, isImplicit); } else { // pixel data as sequence var item = {}; @@ -6464,7 +6325,8 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( }; } // write - byteOffset = this.writeDataElementItems(byteOffset, [item], isImplicit); + byteOffset = this.writeDataElementItems( + writer, byteOffset, [item], isImplicit); } // return new offset @@ -6474,32 +6336,33 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( /** * Write a data element. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {object} element The DICOM data element to write. * @param {number} byteOffset The offset to start writing from. * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeDataElement = function ( - element, byteOffset, isImplicit) { - var isTagWithVR = dwv.dicom.isTagWithVR( - element.tag.group, element.tag.element); +dwv.dicom.DicomWriter.prototype.writeDataElement = function ( + writer, element, byteOffset, isImplicit) { + var isTagWithVR = new dwv.dicom.Tag( + element.tag.group, element.tag.element).isWithVR(); var is32bitVLVR = (isImplicit || !isTagWithVR) ? true : dwv.dicom.is32bitVLVR(element.vr); // group - byteOffset = this.writeHex(byteOffset, element.tag.group); + byteOffset = writer.writeHex(byteOffset, element.tag.group); // element - byteOffset = this.writeHex(byteOffset, element.tag.element); + byteOffset = writer.writeHex(byteOffset, element.tag.element); // VR var vr = element.vr; // use VR=UN for private sequence if (this.useUnVrForPrivateSq && - dwv.dicom.isPrivateGroup(element.tag.group) && + new dwv.dicom.Tag(element.tag.group, element.tag.element).isPrivate() && vr === 'SQ') { dwv.logger.warn('Write element using VR=UN for private sequence.'); vr = 'UN'; } if (isTagWithVR && !isImplicit) { - byteOffset = this.writeString(byteOffset, vr); + byteOffset = writer.writeString(byteOffset, vr); // reserved 2 bytes for 32bit VL if (is32bitVLVR) { byteOffset += 2; @@ -6515,9 +6378,9 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( } // VL if (is32bitVLVR) { - byteOffset = this.writeUint32(byteOffset, vl); + byteOffset = writer.writeUint32(byteOffset, vl); } else { - byteOffset = this.writeUint16(byteOffset, vl); + byteOffset = writer.writeUint16(byteOffset, vl); } // value @@ -6529,10 +6392,10 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( // write if (element.tag.name === 'x7FE00010') { byteOffset = this.writePixelDataElementValue( - element.vr, element.vl, byteOffset, value, isImplicit); + writer, element.vr, element.vl, byteOffset, value, isImplicit); } else { byteOffset = this.writeDataElementValue( - element.vr, element.vl, byteOffset, value, isImplicit); + writer, element.vr, element.vl, byteOffset, value, isImplicit); } // sequence delimitation item for sequence with implicit length @@ -6548,7 +6411,8 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( vl: 0, value: [] }; - byteOffset = this.writeDataElement(seqDelimElement, byteOffset, isImplicit); + byteOffset = this.writeDataElement( + writer, seqDelimElement, byteOffset, isImplicit); } // return new offset @@ -6556,181 +6420,19 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( }; /** - * Is this element an implicit length sequence? - * - * @param {object} element The element to check. - * @returns {boolean} True if it is. - */ -dwv.dicom.isImplicitLengthSequence = function (element) { - // sequence with no length - return (element.vr === 'SQ') && - (element.vl === 'u/l'); -}; - -/** - * Is this element an implicit length item? + * Get the ArrayBuffer corresponding to input DICOM elements. * - * @param {object} element The element to check. - * @returns {boolean} True if it is. + * @param {Array} dicomElements The wrapped elements to write. + * @returns {ArrayBuffer} The elements as a buffer. */ -dwv.dicom.isImplicitLengthItem = function (element) { - // item with no length - return (element.tag.name === 'xFFFEE000') && - (element.vl === 'u/l'); -}; +dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { + // array keys + var keys = Object.keys(dicomElements); -/** - * Is this element an implicit length pixel data? - * - * @param {object} element The element to check. - * @returns {boolean} True if it is. - */ -dwv.dicom.isImplicitLengthPixels = function (element) { - // pixel data with no length - return (element.tag.name === 'x7FE00010') && - (element.vl === 'u/l'); -}; - -/** - * Helper method to flatten an array of typed arrays to 2D typed array - * - * @param {Array} initialArray array of typed arrays - * @returns {object} a typed array containing all values - */ -dwv.dicom.flattenArrayOfTypedArrays = function (initialArray) { - var initialArrayLength = initialArray.length; - var arrayLength = initialArray[0].length; - // If this is not a array of arrays, just return the initial one: - if (typeof arrayLength === 'undefined') { - return initialArray; - } - - var flattenendArrayLength = initialArrayLength * arrayLength; - - var flattenedArray = new initialArray[0].constructor(flattenendArrayLength); - - for (var i = 0; i < initialArrayLength; i++) { - var indexFlattenedArray = i * arrayLength; - flattenedArray.set(initialArray[i], indexFlattenedArray); - } - return flattenedArray; -}; - -/** - * DICOM writer. - * - * @class - */ -dwv.dicom.DicomWriter = function () { - - // flag to use VR=UN for private sequences, default to false - // (mainly used in tests) - this.useUnVrForPrivateSq = false; - - // possible tag actions - var actions = { - copy: function (item) { - return item; - }, - remove: function () { - return null; - }, - clear: function (item) { - item.value[0] = ''; - item.vl = 0; - item.endOffset = item.startOffset; - return item; - }, - replace: function (item, value) { - var paddedValue = dwv.dicom.padElementValue(item, value); - item.value[0] = paddedValue; - item.vl = paddedValue.length; - item.endOffset = item.startOffset + paddedValue.length; - return item; - } - }; - - // default rules: just copy - var defaultRules = { - default: {action: 'copy', value: null} - }; - - /** - * Public (modifiable) rules. - * Set of objects as: - * name : { action: 'actionName', value: 'optionalValue } - * The names are either 'default', tagName or groupName. - * Each DICOM element will be checked to see if a rule is applicable. - * First checked by tagName and then by groupName, - * if nothing is found the default rule is applied. - */ - this.rules = defaultRules; - - /** - * Example anonymisation rules. - */ - this.anonymisationRules = { - default: {action: 'remove', value: null}, - PatientName: {action: 'replace', value: 'Anonymized'}, // tag - 'Meta Element': {action: 'copy', value: null}, // group 'x0002' - Acquisition: {action: 'copy', value: null}, // group 'x0018' - 'Image Presentation': {action: 'copy', value: null}, // group 'x0028' - Procedure: {action: 'copy', value: null}, // group 'x0040' - 'Pixel Data': {action: 'copy', value: null} // group 'x7fe0' - }; - - /** - * Get the element to write according to the class rules. - * Priority order: tagName, groupName, default. - * - * @param {object} element The element to check - * @returns {object} The element to write, can be null. - */ - this.getElementToWrite = function (element) { - // get group and tag string name - var tagName = null; - var dict = dwv.dicom.dictionary; - var group = element.tag.group; - var groupName = dwv.dicom.TagGroups[group.substr(1)]; // remove first 0 - - if (typeof dict[group] !== 'undefined' && - typeof dict[group][element.tag.element] !== 'undefined') { - tagName = dict[group][element.tag.element][2]; - } - // apply rules: - var rule; - if (typeof this.rules[element.tag.name] !== 'undefined') { - // 1. tag itself - rule = this.rules[element.tag.name]; - } else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') { - // 2. tag name - rule = this.rules[tagName]; - } else if (typeof this.rules[groupName] !== 'undefined') { - // 3. group name - rule = this.rules[groupName]; - } else { - // 4. default - rule = this.rules['default']; - } - // apply action on element and return - return actions[rule.action](element, rule.value); - }; -}; - -/** - * Get the ArrayBuffer corresponding to input DICOM elements. - * - * @param {Array} dicomElements The wrapped elements to write. - * @returns {ArrayBuffer} The elements as a buffer. - */ -dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { - // array keys - var keys = Object.keys(dicomElements); - - // transfer syntax - var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]); - var isImplicit = dwv.dicom.isImplicitTransferSyntax(syntax); - var isBigEndian = dwv.dicom.isBigEndianTransferSyntax(syntax); + // transfer syntax + var syntax = dwv.dicom.cleanString(dicomElements.x00020010.value[0]); + var isImplicit = dwv.dicom.isImplicitTransferSyntax(syntax); + var isBigEndian = dwv.dicom.isBigEndianTransferSyntax(syntax); // calculate buffer size and split elements (meta and non meta) var totalSize = 128 + 4; // DICM @@ -6775,11 +6477,6 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { var realVl = element.endOffset - element.startOffset; localSize += parseInt(realVl, 10); - // add size of pixel sequence delimitation items - if (dwv.dicom.isImplicitLengthPixels(element)) { - localSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); - } - // sort elements if (groupName === 'Meta Element') { metaElements.push(element); @@ -6822,14 +6519,20 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { var buffer = new ArrayBuffer(totalSize); var metaWriter = new dwv.dicom.DataWriter(buffer); var dataWriter = new dwv.dicom.DataWriter(buffer, !isBigEndian); + // special character set + if (typeof dicomElements.x00080005 !== 'undefined') { + var scs = dwv.dicom.cleanString(dicomElements.x00080005.value[0]); + dataWriter.setUtfLabel(dwv.dicom.getUtfLabel(scs)); + } + var offset = 128; // DICM offset = metaWriter.writeString(offset, 'DICM'); // FileMetaInformationGroupLength - offset = metaWriter.writeDataElement(fmigl, offset, false); + offset = this.writeDataElement(metaWriter, fmigl, offset, false); // write meta for (var j = 0, lenj = metaElements.length; j < lenj; ++j) { - offset = metaWriter.writeDataElement(metaElements[j], offset, false); + offset = this.writeDataElement(metaWriter, metaElements[j], offset, false); } // check meta position @@ -6838,21 +6541,22 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { if (offset !== metaOffset) { dwv.logger.warn('Bad size calculation... meta offset: ' + offset + ', calculated size:' + metaOffset + - '(diff:', offset - metaOffset, ')'); + ' (diff:' + (offset - metaOffset) + ')'); } // pass flag to writer dataWriter.useUnVrForPrivateSq = this.useUnVrForPrivateSq; // write non meta for (var k = 0, lenk = rawElements.length; k < lenk; ++k) { - offset = dataWriter.writeDataElement(rawElements[k], offset, isImplicit); + offset = this.writeDataElement( + dataWriter, rawElements[k], offset, isImplicit); } // check final position if (offset !== totalSize) { dwv.logger.warn('Bad size calculation... final offset: ' + offset + ', calculated size:' + totalSize + - '(diff:', offset - totalSize, ')'); + ' (diff:' + (offset - totalSize) + ')'); } // return return buffer; @@ -6865,16 +6569,14 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { * @param {object} element The DICOM element. */ dwv.dicom.checkUnknownVR = function (element) { - var dict = dwv.dicom.dictionary; if (element.vr === 'UN') { - if (typeof dict[element.tag.group] !== 'undefined' && - typeof dict[element.tag.group][element.tag.element] !== 'undefined') { - if (element.vr !== dict[element.tag.group][element.tag.element][0]) { - element.vr = dict[element.tag.group][element.tag.element][0]; - dwv.logger.info('Element ' + element.tag.group + - ' ' + element.tag.element + - ' VR changed from UN to ' + element.vr); - } + var tag = new dwv.dicom.Tag(element.tag.group, element.tag.element); + var dictVr = tag.getVrFromDictionary(); + if (dictVr !== null && element.vr !== dictVr) { + element.vr = dictVr; + dwv.logger.info('Element ' + element.tag.group + + ' ' + element.tag.element + + ' VR changed from UN to ' + element.vr); } } }; @@ -6886,12 +6588,11 @@ dwv.dicom.checkUnknownVR = function (element) { * @returns {object} The DICOM element. */ dwv.dicom.getDicomElement = function (tagName) { - var tagGE = dwv.dicom.getGroupElementFromName(tagName); - var dict = dwv.dicom.dictionary; + var tag = dwv.dicom.getTagFromDictionary(tagName); // return element definition return { - tag: {group: tagGE.group, element: tagGE.element}, - vr: dict[tagGE.group][tagGE.element][0] + tag: {group: tag.getGroup(), element: tag.getElement()}, + vr: tag.getVrFromDictionary() }; }; @@ -6945,8 +6646,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { subSize += dwv.dicom.setElementValue( subElement, itemData[elemKeys[j]]); - name = dwv.dicom.getGroupElementKey( - subElement.tag.group, subElement.tag.element); + name = new dwv.dicom.Tag( + subElement.tag.group, subElement.tag.element).getKey(); itemElements[name] = subElement; subSize += dwv.dicom.getDataElementPrefixByteSize( subElement.vr, isImplicit); @@ -6959,8 +6660,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { vl: (explicitLength ? subSize : 'u/l'), value: [] }; - name = dwv.dicom.getGroupElementKey( - itemElement.tag.group, itemElement.tag.element); + name = new dwv.dicom.Tag( + itemElement.tag.group, itemElement.tag.element).getKey(); itemElements[name] = itemElement; subSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); @@ -6972,8 +6673,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { vl: 0, value: [] }; - name = dwv.dicom.getGroupElementKey( - itemDelimElement.tag.group, itemDelimElement.tag.element); + name = new dwv.dicom.Tag( + itemDelimElement.tag.group, itemDelimElement.tag.element).getKey(); itemElements[name] = itemDelimElement; subSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); } @@ -11333,6 +11034,7 @@ dwv.dicom.TagGroups = { // namespaces var dwv = dwv || {}; +/** @namespace */ dwv.gui = dwv.gui || {}; /** @@ -11346,7 +11048,8 @@ var Konva = Konva || {}; /** * Draw layer. * - * @param {object} containerDiv The layer div. + * @param {HTMLElement} containerDiv The layer div, its id will be used + * as this layer id. * @class */ dwv.gui.DrawLayer = function (containerDiv) { @@ -11354,447 +11057,1569 @@ dwv.gui.DrawLayer = function (containerDiv) { // specific css class name containerDiv.className += ' drawLayer'; + // closure to self + var self = this; + // konva stage var konvaStage = null; // konva layer var konvaLayer; /** - * The layer size as {x,y}. - * - * @private - * @type {object} + * The layer base size as {x,y}. + * + * @private + * @type {object} + */ + var baseSize; + + /** + * The layer base spacing as {x,y}. + * + * @private + * @type {object} + */ + var baseSpacing; + + /** + * The layer fit scale. + * + * @private + * @type {object} + */ + var fitScale = {x: 1, y: 1}; + + /** + * The base layer offset. + * + * @private + * @type {object} + */ + var baseOffset = {x: 0, y: 0}; + + /** + * The draw controller. + * + * @private + * @type {object} + */ + var drawController = null; + + /** + * The plane helper. + * + * @private + * @type {object} + */ + var planeHelper; + + /** + * The associated data index. + * + * @private + * @type {number} + */ + var dataIndex = null; + + /** + * Get the associated data index. + * + * @returns {number} The index. + */ + this.getDataIndex = function () { + return dataIndex; + }; + + /** + * Listener handler. + * + * @type {object} + * @private + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Get the Konva stage. + * + * @returns {object} The stage. + */ + this.getKonvaStage = function () { + return konvaStage; + }; + + /** + * Get the Konva layer. + * + * @returns {object} The layer. + */ + this.getKonvaLayer = function () { + return konvaLayer; + }; + + /** + * Get the draw controller. + * + * @returns {object} The controller. + */ + this.getDrawController = function () { + return drawController; + }; + + /** + * Set the plane helper. + * + * @param {object} helper The helper. + */ + this.setPlaneHelper = function (helper) { + planeHelper = helper; + }; + + // common layer methods [start] --------------- + + /** + * Get the id of the layer. + * + * @returns {string} The string id. + */ + this.getId = function () { + return containerDiv.id; + }; + + /** + * Get the data full size, ie size * spacing. + * + * @returns {object} The full size as {x,y}. + */ + this.getFullSize = function () { + return { + x: baseSize.x * baseSpacing.x, + y: baseSize.y * baseSpacing.y + }; + }; + + /** + * Get the layer base size (without scale). + * + * @returns {object} The size as {x,y}. + */ + this.getBaseSize = function () { + return baseSize; + }; + + /** + * Get the layer opacity. + * + * @returns {number} The opacity ([0:1] range). + */ + this.getOpacity = function () { + return konvaStage.opacity(); + }; + + /** + * Set the layer opacity. + * + * @param {number} alpha The opacity ([0:1] range). + */ + this.setOpacity = function (alpha) { + konvaStage.opacity(Math.min(Math.max(alpha, 0), 1)); + }; + + /** + * Set the layer scale. + * + * @param {object} newScale The scale as {x,y}. + */ + this.setScale = function (newScale) { + var orientedNewScale = planeHelper.getOrientedXYZ(newScale); + var fullScale = { + x: fitScale.x * orientedNewScale.x, + y: fitScale.y * orientedNewScale.y + }; + konvaStage.scale(fullScale); + // update labelss + updateLabelScale(fullScale); + }; + + /** + * Set the layer offset. + * + * @param {object} newOffset The offset as {x,y}. + */ + this.setOffset = function (newOffset) { + var planeNewOffset = planeHelper.getPlaneOffsetFromOffset3D(newOffset); + konvaStage.offset({ + x: baseOffset.x + planeNewOffset.x, + y: baseOffset.y + planeNewOffset.y + }); + }; + + /** + * Set the base layer offset. Resets the layer offset. + * + * @param {object} off The offset as {x,y}. + */ + this.setBaseOffset = function (off) { + baseOffset = planeHelper.getPlaneOffsetFromOffset3D({ + x: off.getX(), + y: off.getY(), + z: off.getZ() + }); + // reset offset + konvaStage.offset({ + x: baseOffset.x, + y: baseOffset.y + }); + }; + + /** + * Display the layer. + * + * @param {boolean} flag Whether to display the layer or not. + */ + this.display = function (flag) { + containerDiv.style.display = flag ? '' : 'none'; + }; + + /** + * Check if the layer is visible. + * + * @returns {boolean} True if the layer is visible. + */ + this.isVisible = function () { + return containerDiv.style.display === ''; + }; + + /** + * Draw the content (imageData) of the layer. + * The imageData variable needs to be set + */ + this.draw = function () { + konvaStage.draw(); + }; + + /** + * Initialise the layer: set the canvas and context + * + * @param {object} size The image size as {x,y}. + * @param {object} spacing The image spacing as {x,y}. + * @param {number} index The associated data index. + */ + this.initialise = function (size, spacing, index) { + // set locals + baseSize = size; + baseSpacing = spacing; + dataIndex = index; + + // create stage + konvaStage = new Konva.Stage({ + container: containerDiv, + width: baseSize.x, + height: baseSize.y, + listening: false + }); + // reset style + // (avoids a not needed vertical scrollbar) + konvaStage.getContent().setAttribute('style', ''); + + // create layer + konvaLayer = new Konva.Layer({ + listening: false, + visible: true + }); + konvaStage.add(konvaLayer); + + // create draw controller + drawController = new dwv.ctrl.DrawController(konvaLayer); + }; + + /** + * Fit the layer to its parent container. + * + * @param {number} fitScale1D The 1D fit scale. + */ + this.fitToContainer = function (fitScale1D) { + // update fit scale + fitScale = { + x: fitScale1D * baseSpacing.x, + y: fitScale1D * baseSpacing.y + }; + // update konva + var fullSize = this.getFullSize(); + var width = Math.floor(fullSize.x * fitScale1D); + var height = Math.floor(fullSize.y * fitScale1D); + konvaStage.setWidth(width); + konvaStage.setHeight(height); + // reset scale + this.setScale({x: 1, y: 1, z: 1}); + }; + + /** + * Enable and listen to container interaction events. + */ + this.bindInteraction = function () { + konvaStage.listening(true); + // allow pointer events + containerDiv.style.pointerEvents = 'auto'; + // interaction events + var names = dwv.gui.interactionEventNames; + for (var i = 0; i < names.length; ++i) { + containerDiv.addEventListener(names[i], fireEvent); + } + }; + + /** + * Disable and stop listening to container interaction events. + */ + this.unbindInteraction = function () { + konvaStage.listening(false); + // disable pointer events + containerDiv.style.pointerEvents = 'none'; + // interaction events + var names = dwv.gui.interactionEventNames; + for (var i = 0; i < names.length; ++i) { + containerDiv.removeEventListener(names[i], fireEvent); + } + }; + + /** + * Set the current position. + * + * @param {dwv.math.Point} position The new position. + * @param {dwv.math.Index} index The new index. + */ + this.setCurrentPosition = function (position, index) { + this.getDrawController().activateDrawLayer( + index, planeHelper.getScrollIndex()); + }; + + /** + * Add an event listener to this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + + /** + * Remove an event listener from this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + + /** + * Fire an event: call all associated listeners with the input event object. + * + * @param {object} event The event to fire. + * @private + */ + function fireEvent(event) { + event.srclayerid = self.getId(); + event.dataindex = dataIndex; + listenerHandler.fireEvent(event); + } + + // common layer methods [end] --------------- + + /** + * Update label scale: compensate for it so + * that label size stays visually the same. + * + * @param {object} scale The scale to compensate for as {x,y}. + */ + function updateLabelScale(scale) { + // same formula as in style::applyZoomScale: + // compensate for scale and times 2 so that font 10 looks like a 10 + var ratioX = 2 / scale.x; + var ratioY = 2 / scale.y; + // compensate scale for labels + var labels = konvaStage.find('Label'); + for (var i = 0; i < labels.length; ++i) { + labels[i].scale({x: ratioX, y: ratioY}); + } + } +}; // DrawLayer class + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; + +/** + * List of interaction event names. + */ +dwv.gui.interactionEventNames = [ + 'mousedown', + 'mousemove', + 'mouseup', + 'mouseout', + 'wheel', + 'dblclick', + 'touchstart', + 'touchmove', + 'touchend' +]; + +/** + * Get a HTML element associated to a container div. + * + * @param {number} containerDivId The id of the container div. + * @param {string} name The name or id to find. + * @returns {object} The found element or null. + * @deprecated + */ +dwv.gui.getElement = function (containerDivId, name) { + // get by class in the container div + var parent = document.getElementById(containerDivId); + if (!parent) { + return null; + } + var elements = parent.getElementsByClassName(name); + // getting the last element since some libraries (ie jquery-mobile) create + // span in front of regular tags (such as select)... + var element = elements[elements.length - 1]; + // if not found get by id with 'containerDivId-className' + if (typeof element === 'undefined') { + element = document.getElementById(containerDivId + '-' + name); + } + return element; +}; + +/** + * Get a HTML element associated to a container div. Defaults to local one. + * + * @see dwv.gui.getElement + * @deprecated + */ +dwv.getElement = dwv.gui.getElement; + +/** + * Prompt the user for some text. Uses window.prompt. + * + * @param {string} message The message in front of the input field. + * @param {string} value The input default value. + * @returns {string} The new value. + */ +dwv.gui.prompt = function (message, value) { + return prompt(message, value); +}; + +/** + * Prompt the user for some text. Defaults to local one. + * + * @see dwv.gui.prompt + */ +dwv.prompt = dwv.gui.prompt; + +/** + * Open a dialogue to edit roi data. Defaults to undefined. + * + * @param {object} data The roi data. + * @param {Function} callback The callback to launch on dialogue exit. + * @see dwv.tool.Draw + */ +dwv.openRoiDialog; + +/** + * Get the positions (without the parent offset) of a list of touch events. + * + * @param {Array} touches The list of touch events. + * @returns {Array} The list of positions of the touch events. + */ +dwv.gui.getTouchesPositions = function (touches) { + // get the touch offset from all its parents + var offsetLeft = 0; + var offsetTop = 0; + if (touches.length !== 0 && + typeof touches[0].target !== 'undefined') { + var offsetParent = touches[0].target.offsetParent; + while (offsetParent) { + if (!isNaN(offsetParent.offsetLeft)) { + offsetLeft += offsetParent.offsetLeft; + } + if (!isNaN(offsetParent.offsetTop)) { + offsetTop += offsetParent.offsetTop; + } + offsetParent = offsetParent.offsetParent; + } + } else { + dwv.logger.debug('No touch target offset parent.'); + } + // set its position + var positions = []; + for (var i = 0; i < touches.length; ++i) { + positions.push({ + x: touches[i].pageX - offsetLeft, + y: touches[i].pageY - offsetTop + }); + } + return positions; +}; + +/** + * Get the offset of an input event. + * + * @param {object} event The event to get the offset from. + * @returns {Array} The array of offsets. + */ +dwv.gui.getEventOffset = function (event) { + var positions = []; + if (typeof event.targetTouches !== 'undefined' && + event.targetTouches.length !== 0) { + // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/targetTouches + positions = dwv.gui.getTouchesPositions(event.targetTouches); + } else if (typeof event.changedTouches !== 'undefined' && + event.changedTouches.length !== 0) { + // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/changedTouches + positions = dwv.gui.getTouchesPositions(event.changedTouches); + } else { + // offsetX/Y: the offset in the X coordinate of the mouse pointer + // between that event and the padding edge of the target node + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX + // https://caniuse.com/mdn-api_mouseevent_offsetx + positions.push({ + x: event.offsetX, + y: event.offsetY + }); + } + return positions; +}; + +/** + * Test if a canvas with the input size can be created. + * + * @see https://github.com/ivmartel/dwv/issues/902 + * @see https://github.com/jhildenbiddle/canvas-size/blob/v1.2.4/src/canvas-test.js + * @param {number} width The canvas width. + * @param {number} height The canvas height. + * @returns {boolean} True is the canvas can be created. + */ +dwv.gui.canCreateCanvas = function (width, height) { + // test canvas with input size + var testCvs = document.createElement('canvas'); + testCvs.width = width; + testCvs.height = height; + // crop canvas to speed up test + var cropCvs = document.createElement('canvas'); + cropCvs.width = 1; + cropCvs.height = 1; + // contexts + var testCtx = testCvs.getContext('2d'); + var cropCtx = cropCvs.getContext('2d'); + // set data + if (testCtx) { + testCtx.fillRect(width - 1, height - 1, 1, 1); + // Render the test pixel in the bottom-right corner of the + // test canvas in the top-left of the 1x1 crop canvas. This + // dramatically reducing the time for getImageData to complete. + cropCtx.drawImage(testCvs, width - 1, height - 1, 1, 1, 0, 0, 1, 1); + } + // Verify image data (alpha component, Pass = 255, Fail = 0) + return cropCtx && cropCtx.getImageData(0, 0, 1, 1).data[3] !== 0; +}; + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; + +/** + * Get the layer group div id. + * + * @param {number} groupId The layer group id. + * @param {number} layerId The lyaer id. + * @returns {string} A string id. + */ +dwv.gui.getLayerGroupDivId = function (groupId, layerId) { + return 'layer-' + groupId + '-' + layerId; +}; + +/** + * Get the layer details from a div id. + * + * @param {string} idString The layer group id. + * @returns {object} The layer details as {groupId, layerId}. + */ +dwv.gui.getLayerDetailsFromLayerDivId = function (idString) { + var posHyphen = idString.lastIndexOf('-'); + var groupId = null; + var layerId = null; + if (posHyphen !== -1) { + groupId = parseInt(idString.substring(6, posHyphen), 10); + layerId = parseInt(idString.substring(posHyphen + 1), 10); + } + return { + groupId: groupId, + layerId: layerId + }; +}; + +/** + * Get the layer details from a mouse event. + * + * @param {object} event The event to get the layer div id from. Expecting + * an event origininating from a canvas inside a layer HTML div + * with the 'layer' class and id generated with `dwv.gui.getLayerGroupDivId`. + * @returns {object} The layer details as {groupId, layerId}. + */ +dwv.gui.getLayerDetailsFromEvent = function (event) { + var res = null; + // get the closest element from the event target and with the 'layer' class + var layerDiv = event.target.closest('.layer'); + if (layerDiv && typeof layerDiv.id !== 'undefined') { + res = dwv.gui.getLayerDetailsFromLayerDivId(layerDiv.id); + } + return res; +}; + +/** + * Get a view orientation according to an image geometry (with its orientation) + * and target orientation. + * + * @param {dwv.image.Geometry} imageGeometry The image geometry. + * @param {dwv.math.Matrix33} targetOrientation The target orientation. + * @returns {dwv.math.Matrix33} The view orientation. + */ +dwv.gui.getViewOrientation = function (imageGeometry, targetOrientation) { + var viewOrientation = dwv.math.getIdentityMat33(); + if (typeof targetOrientation !== 'undefined') { + // image orientation as one and zeros + // -> view orientation is one and zeros + var imgOrientation = imageGeometry.getOrientation().asOneAndZeros(); + // imgOrientation * viewOrientation = targetOrientation + // -> viewOrientation = inv(imgOrientation) * targetOrientation + viewOrientation = + imgOrientation.getInverse().multiply(targetOrientation); + } + return viewOrientation; +}; + +/** + * Layer group. + * + * Display position: {x,y} + * Plane position: Index (access: get(i)) + * (world) Position: Point3D (access: getX, getY, getZ) + * + * Display -> World: + * planePos = viewLayer.displayToPlanePos(displayPos) + * -> compensate for layer scale and offset + * pos = viewController.getPositionFromPlanePoint(planePos) + * + * World -> display + * planePos = viewController.getOffset3DFromPlaneOffset(pos) + * no need yet for a planePos to displayPos... + * + * @param {object} containerDiv The associated HTML div. + * @param {number} groupId The group id. + * @class + */ +dwv.gui.LayerGroup = function (containerDiv, groupId) { + + // closure to self + var self = this; + // list of layers + var layers = []; + + /** + * The layer scale as {x,y}. + * + * @private + * @type {object} + */ + var scale = {x: 1, y: 1, z: 1}; + + /** + * The base scale as {x,y}: all posterior scale will be on top of this one. + * + * @private + * @type {object} + */ + var baseScale = {x: 1, y: 1, z: 1}; + + /** + * The layer offset as {x,y}. + * + * @private + * @type {object} + */ + var offset = {x: 0, y: 0, z: 0}; + + /** + * Active view layer index. + * + * @private + * @type {number} + */ + var activeViewLayerIndex = null; + + /** + * Active draw layer index. + * + * @private + * @type {number} + */ + var activeDrawLayerIndex = null; + + /** + * Listener handler. + * + * @type {object} + * @private + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * The target orientation matrix. + * + * @type {object} + * @private + */ + var targetOrientation; + + /** + * Get the target orientation. + * + * @returns {dwv.math.Matrix33} The orientation matrix. + */ + this.getTargetOrientation = function () { + return targetOrientation; + }; + + /** + * Set the target orientation. + * + * @param {dwv.math.Matrix33} orientation The orientation matrix. + */ + this.setTargetOrientation = function (orientation) { + targetOrientation = orientation; + }; + + /** + * Get the Id of the container div. + * + * @returns {string} The id of the div. + */ + this.getElementId = function () { + return containerDiv.id; + }; + + /** + * Get the layer group id. + * + * @returns {number} The id. + */ + this.getGroupId = function () { + return groupId; + }; + + /** + * Get the layer scale. + * + * @returns {object} The scale as {x,y,z}. + */ + this.getScale = function () { + return scale; + }; + + /** + * Get the base scale. + * + * @returns {object} The scale as {x,y,z}. + */ + this.getBaseScale = function () { + return baseScale; + }; + + /** + * Get the added scale: the scale added to the base scale + * + * @returns {object} The scale as {x,y,z}. + */ + this.getAddedScale = function () { + return { + x: scale.x / baseScale.x, + y: scale.y / baseScale.y, + z: scale.z / baseScale.z + }; + }; + + /** + * Get the layer offset. + * + * @returns {object} The offset as {x,y,z}. + */ + this.getOffset = function () { + return offset; + }; + + /** + * Get the number of layers handled by this class. + * + * @returns {number} The number of layers. + */ + this.getNumberOfLayers = function () { + return layers.length; + }; + + /** + * Get the active image layer. + * + * @returns {object} The layer. + */ + this.getActiveViewLayer = function () { + return layers[activeViewLayerIndex]; + }; + + /** + * Get the view layers associated to a data index. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getViewLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.ViewLayer && + layers[i].getDataIndex() === index) { + res.push(layers[i]); + } + } + return res; + }; + + /** + * Get the active draw layer. + * + * @returns {object} The layer. + */ + this.getActiveDrawLayer = function () { + return layers[activeDrawLayerIndex]; + }; + + /** + * Get the draw layers associated to a data index. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getDrawLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.DrawLayer && + layers[i].getDataIndex() === index) { + res.push(layers[i]); + } + } + return res; + }; + + /** + * Set the active view layer. + * + * @param {number} index The index of the layer to set as active. + */ + this.setActiveViewLayer = function (index) { + activeViewLayerIndex = index; + }; + + /** + * Set the active view layer with a data index. + * + * @param {number} index The data index. + */ + this.setActiveViewLayerByDataIndex = function (index) { + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.ViewLayer && + layers[i].getDataIndex() === index) { + this.setActiveViewLayer(i); + break; + } + } + }; + + /** + * Set the active draw layer. + * + * @param {number} index The index of the layer to set as active. + */ + this.setActiveDrawLayer = function (index) { + activeDrawLayerIndex = index; + }; + + /** + * Set the active draw layer with a data index. + * + * @param {number} index The data index. + */ + this.setActiveDrawLayerByDataIndex = function (index) { + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.DrawLayer && + layers[i].getDataIndex() === index) { + this.setActiveDrawLayer(i); + break; + } + } + }; + + /** + * Add a view layer. + * + * @returns {object} The created layer. + */ + this.addViewLayer = function () { + // layer index + var viewLayerIndex = layers.length; + // create div + var div = getNextLayerDiv(); + // prepend to container + containerDiv.append(div); + // view layer + var layer = new dwv.gui.ViewLayer(div); + // add layer + layers.push(layer); + // mark it as active + this.setActiveViewLayer(viewLayerIndex); + // bind view layer events + bindViewLayer(layer); + // return + return layer; + }; + + /** + * Add a draw layer. + * + * @returns {object} The created layer. + */ + this.addDrawLayer = function () { + // store active index + activeDrawLayerIndex = layers.length; + // create div + var div = getNextLayerDiv(); + // prepend to container + containerDiv.append(div); + // draw layer + var layer = new dwv.gui.DrawLayer(div); + // add layer + layers.push(layer); + // return + return layer; + }; + + /** + * Bind view layer events to this. + * + * @param {object} viewLayer The view layer to bind. + */ + function bindViewLayer(viewLayer) { + // listen to position change to update other group layers + viewLayer.addEventListener( + 'positionchange', self.updateLayersToPositionChange); + // propagate view viewLayer-layer events + for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { + viewLayer.addEventListener(dwv.image.viewEventNames[j], fireEvent); + } + // propagate viewLayer events + viewLayer.addEventListener('renderstart', fireEvent); + viewLayer.addEventListener('renderend', fireEvent); + } + + /** + * Get the next layer DOM div. + * + * @returns {HTMLElement} A DOM div. + */ + function getNextLayerDiv() { + var div = document.createElement('div'); + div.id = dwv.gui.getLayerGroupDivId(groupId, layers.length); + div.className = 'layer'; + div.style.pointerEvents = 'none'; + return div; + } + + /** + * Empty the layer list. + */ + this.empty = function () { + layers = []; + // reset active indices + activeViewLayerIndex = null; + activeDrawLayerIndex = null; + // clean container div + var previous = containerDiv.getElementsByClassName('layer'); + if (previous) { + while (previous.length > 0) { + previous[0].remove(); + } + } + }; + + /** + * Update layers (but not the active view layer) to a position change. + * + * @param {object} event The position change event. + */ + this.updateLayersToPositionChange = function (event) { + // pause positionchange listeners + for (var j = 0; j < layers.length; ++j) { + if (layers[j] instanceof dwv.gui.ViewLayer) { + layers[j].removeEventListener( + 'positionchange', self.updateLayersToPositionChange); + layers[j].removeEventListener('positionchange', fireEvent); + } + } + + var index = new dwv.math.Index(event.value[0]); + var position = new dwv.math.Point(event.value[1]); + // update position for all layers except the source one + for (var i = 0; i < layers.length; ++i) { + if (layers[i].getId() !== event.srclayerid) { + layers[i].setCurrentPosition(position, index); + } + } + + // re-start positionchange listeners + for (var k = 0; k < layers.length; ++k) { + if (layers[k] instanceof dwv.gui.ViewLayer) { + layers[k].addEventListener( + 'positionchange', self.updateLayersToPositionChange); + layers[k].addEventListener('positionchange', fireEvent); + } + } + }; + + /** + * Fit the display to the size of the container. + * To be called once the image is loaded. */ - var layerSize; + this.fitToContainer = function () { + // check container size + if (containerDiv.offsetWidth === 0 && + containerDiv.offsetHeight === 0) { + throw new Error('Cannot fit to zero sized container.'); + } + // find best fit + var fitScales = []; + for (var i = 0; i < layers.length; ++i) { + var fullSize = layers[i].getFullSize(); + fitScales.push(containerDiv.offsetWidth / fullSize.x); + fitScales.push(containerDiv.offsetHeight / fullSize.y); + } + var fitScale = Math.min.apply(null, fitScales); + // apply to layers + for (var j = 0; j < layers.length; ++j) { + layers[j].fitToContainer(fitScale); + } + }; /** - * The draw controller. + * Add scale to the layers. Scale cannot go lower than 0.1. * - * @private - * @type {object} + * @param {number} scaleStep The scale to add. + * @param {dwv.math.Point3D} center The scale center Point3D. */ - var drawController = null; + this.addScale = function (scaleStep, center) { + var newScale = { + x: scale.x * (1 + scaleStep), + y: scale.y * (1 + scaleStep), + z: scale.z * (1 + scaleStep) + }; + var centerPlane = { + x: (center.getX() - offset.x) * scale.x, + y: (center.getY() - offset.y) * scale.y, + z: (center.getZ() - offset.z) * scale.z + }; + // center should stay the same: + // center / newScale + newOffset = center / oldScale + oldOffset + // => newOffset = center / oldScale + oldOffset - center / newScale + var newOffset = { + x: (centerPlane.x / scale.x) + offset.x - (centerPlane.x / newScale.x), + y: (centerPlane.y / scale.y) + offset.y - (centerPlane.y / newScale.y), + z: (centerPlane.z / scale.z) + offset.z - (centerPlane.z / newScale.z) + }; + + this.setOffset(newOffset); + this.setScale(newScale); + }; /** - * Listener handler. + * Set the layers' scale. * - * @type {object} - * @private + * @param {object} newScale The scale to apply as {x,y,z}. + * @fires dwv.ctrl.LayerGroup#zoomchange */ - var listenerHandler = new dwv.utils.ListenerHandler(); + this.setScale = function (newScale) { + scale = newScale; + // apply to layers + for (var i = 0; i < layers.length; ++i) { + layers[i].setScale(scale); + } + + /** + * Zoom change event. + * + * @event dwv.ctrl.LayerGroup#zoomchange + * @type {object} + * @property {Array} value The changed value. + */ + fireEvent({ + type: 'zoomchange', + value: [scale.x, scale.y, scale.z], + }); + }; /** - * Get the Konva stage. + * Add translation to the layers. * - * @returns {object} The stage. + * @param {object} translation The translation as {x,y,z}. */ - this.getKonvaStage = function () { - return konvaStage; + this.addTranslation = function (translation) { + this.setOffset({ + x: offset.x - translation.x, + y: offset.y - translation.y, + z: offset.z - translation.z + }); }; /** - * Get the Konva layer. + * Set the layers' offset. * - * @returns {object} The layer. + * @param {object} newOffset The offset as {x,y,z}. + * @fires dwv.ctrl.LayerGroup#offsetchange */ - this.getKonvaLayer = function () { - return konvaLayer; + this.setOffset = function (newOffset) { + // store + offset = newOffset; + // apply to layers + for (var i = 0; i < layers.length; ++i) { + layers[i].setOffset(offset); + } + + /** + * Offset change event. + * + * @event dwv.ctrl.LayerGroup#offsetchange + * @type {object} + * @property {Array} value The changed value. + */ + fireEvent({ + type: 'offsetchange', + value: [offset.x, offset.y, offset.z], + }); }; /** - * Get the draw controller. - * - * @returns {object} The controller. + * Reset the stage to its initial scale and no offset. */ - this.getDrawController = function () { - return drawController; + this.reset = function () { + this.setScale(baseScale); + this.setOffset({x: 0, y: 0, z: 0}); }; - // common layer methods [start] --------------- - /** - * Get the layer size. - * - * @returns {object} The size as {x,y}. + * Draw the layer. */ - this.getSize = function () { - return layerSize; + this.draw = function () { + for (var i = 0; i < layers.length; ++i) { + layers[i].draw(); + } }; /** - * Get the layer opacity. + * Display the layer. * - * @returns {number} The opacity ([0:1] range). + * @param {boolean} flag Whether to display the layer or not. */ - this.getOpacity = function () { - return konvaStage.opacity(); + this.display = function (flag) { + for (var i = 0; i < layers.length; ++i) { + layers[i].display(flag); + } }; /** - * Set the layer opacity. + * Add an event listener to this class. * - * @param {number} alpha The opacity ([0:1] range). + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. */ - this.setOpacity = function (alpha) { - konvaStage.opacity(Math.min(Math.max(alpha, 0), 1)); + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); }; /** - * Set the layer scale. + * Remove an event listener from this class. * - * @param {object} newScale The scale as {x,y}. + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. */ - this.setScale = function (newScale) { - konvaStage.scale(newScale); - // update labels - updateLabelScale(newScale); + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); }; /** - * Set the layer offset. + * Fire an event: call all associated listeners with the input event object. * - * @param {object} newOffset The offset as {x,y}. + * @param {object} event The event to fire. + * @private */ - this.setOffset = function (newOffset) { - konvaStage.offset(newOffset); + function fireEvent(event) { + listenerHandler.fireEvent(event); + } + +}; // LayerGroup class + +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; + +/** + * Window/level binder. + */ +dwv.gui.WindowLevelBinder = function () { + this.getEventType = function () { + return 'wlchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + var viewLayers = layerGroup.getViewLayersByDataIndex(event.dataindex); + if (viewLayers.length !== 0) { + var vc = viewLayers[0].getViewController(); + vc.setWindowLevel(event.value[0], event.value[1]); + } + }; + }; +}; + +/** + * Position binder. + */ +dwv.gui.PositionBinder = function () { + this.getEventType = function () { + return 'positionchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + var pos = new dwv.math.Point(event.value[1]); + var vc = layerGroup.getActiveViewLayer().getViewController(); + vc.setCurrentPosition(pos); + }; + }; +}; + +/** + * Zoom binder. + */ +dwv.gui.ZoomBinder = function () { + this.getEventType = function () { + return 'zoomchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + layerGroup.setScale({ + x: event.value[0], + y: event.value[1], + z: event.value[2] + }); + layerGroup.draw(); + }; + }; +}; + +/** + * Offset binder. + */ +dwv.gui.OffsetBinder = function () { + this.getEventType = function () { + return 'offsetchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + layerGroup.setOffset({ + x: event.value[0], + y: event.value[1], + z: event.value[2] + }); + layerGroup.draw(); + }; + }; +}; + +/** + * Opacity binder. Only propagates to view layers of the same data. + */ +dwv.gui.OpacityBinder = function () { + this.getEventType = function () { + return 'opacitychange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + // exit if no data index + if (typeof event.dataindex === 'undefined') { + return; + } + // propagate to first view layer + var viewLayers = layerGroup.getViewLayersByDataIndex(event.dataindex); + if (viewLayers.length !== 0) { + viewLayers[0].setOpacity(event.value); + viewLayers[0].draw(); + } + }; }; +}; + +/** + * Stage: controls a list of layer groups and their + * synchronisation. + * + * @class + */ +dwv.gui.Stage = function () { + + // associated layer groups + var layerGroups = []; + // active layer group index + var activeLayerGroupIndex = null; + + // layer group binders + var binders = []; + // binder callbacks + var callbackStore = null; /** - * Set the layer z-index. + * Get the layer group at the given index. * * @param {number} index The index. + * @returns {dwv.gui.LayerGroup} The layer group. */ - this.setZIndex = function (index) { - containerDiv.style.zIndex = index; + this.getLayerGroup = function (index) { + return layerGroups[index]; }; /** - * Resize the layer: update the window scale and layer sizes. + * Get the number of layer groups that form the stage. * - * @param {object} newScale The layer scale as {x,y}. + * @returns {number} The number of layer groups. */ - this.resize = function (newScale) { - // resize stage - konvaStage.setWidth(parseInt(layerSize.x * newScale.x, 10)); - konvaStage.setHeight(parseInt(layerSize.y * newScale.y, 10)); - // set scale - this.setScale(newScale); + this.getNumberOfLayerGroups = function () { + return layerGroups.length; }; /** - * Display the layer. + * Get the active layer group. * - * @param {boolean} flag Whether to display the layer or not. + * @returns {dwv.gui.LayerGroup} The layer group. */ - this.display = function (flag) { - containerDiv.style.display = flag ? '' : 'none'; + this.getActiveLayerGroup = function () { + return this.getLayerGroup(activeLayerGroupIndex); }; /** - * Check if the layer is visible. + * Get the view layers associated to a data index. * - * @returns {boolean} True if the layer is visible. + * @param {number} index The data index. + * @returns {Array} The layers. */ - this.isVisible = function () { - return containerDiv.style.display === ''; + this.getViewLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layerGroups.length; ++i) { + res = res.concat(layerGroups[i].getViewLayersByDataIndex(index)); + } + return res; }; /** - * Draw the content (imageData) of the layer. - * The imageData variable needs to be set + * Add a layer group to the list. + * + * @param {object} htmlElement The HTML element of the layer group. + * @returns {dwv.gui.LayerGroup} The newly created layer group. */ - this.draw = function () { - konvaStage.draw(); + this.addLayerGroup = function (htmlElement) { + activeLayerGroupIndex = layerGroups.length; + var layerGroup = new dwv.gui.LayerGroup(htmlElement, activeLayerGroupIndex); + // add to storage + var isBound = callbackStore && callbackStore.length !== 0; + if (isBound) { + this.unbindLayerGroups(); + } + layerGroups.push(layerGroup); + if (isBound) { + this.bindLayerGroups(); + } + // return created group + return layerGroup; }; /** - * Initialise the layer: set the canvas and context + * Get a layer group from an HTML element id. * - * @param {object} image The image. - * @param {object} _metaData The image meta data. + * @param {string} id The element id to find. + * @returns {dwv.gui.LayerGroup} The layer group. */ - this.initialise = function (image, _metaData) { - // get sizes - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; - - // create stage - konvaStage = new Konva.Stage({ - container: containerDiv, - width: layerSize.x, - height: layerSize.y, - listening: false - }); - // reset style - // (avoids a not needed vertical scrollbar) - konvaStage.getContent().setAttribute('style', ''); - - // create layer - konvaLayer = new Konva.Layer({ - listening: false, - visible: true + this.getLayerGroupWithElementId = function (id) { + return layerGroups.find(function (item) { + return item.getElementId() === id; }); - konvaStage.add(konvaLayer); - - // create draw controller - drawController = new dwv.DrawController(konvaLayer); }; /** - * Update the layer position. + * Set the layer groups binders. * - * @param {object} pos The new position. + * @param {Array} list The list of binder objects. */ - this.updatePosition = function (pos) { - this.getDrawController().activateDrawLayer(pos[0], pos[1]); + this.setBinders = function (list) { + if (typeof list === 'undefined' || list === null) { + throw new Error('Cannot set null or undefined binders'); + } + if (binders.length !== 0) { + this.unbindLayerGroups(); + } + binders = list.slice(); + this.bindLayerGroups(); }; /** - * Activate the layer: propagate events. + * Empty the layer group list. */ - this.activate = function () { - konvaStage.listening(true); - // allow pointer events - containerDiv.style.pointerEvents = 'auto'; - // interaction events - var names = dwv.gui.interactionEventNames; - for (var i = 0; i < names.length; ++i) { - containerDiv.addEventListener(names[i], fireEvent); + this.empty = function () { + this.unbindLayerGroups(); + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].empty(); } + layerGroups = []; + activeLayerGroupIndex = null; }; /** - * Deactivate the layer: stop propagating events. + * Reset the stage: calls reset on all layer groups. */ - this.deactivate = function () { - konvaStage.listening(false); - // disable pointer events - containerDiv.style.pointerEvents = 'none'; - // interaction events - var names = dwv.gui.interactionEventNames; - for (var i = 0; i < names.length; ++i) { - containerDiv.removeEventListener(names[i], fireEvent); + this.reset = function () { + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].reset(); + } + }; + + /** + * Draw the stage: calls draw on all layer groups. + */ + this.draw = function () { + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].draw(); } }; /** - * Add an event listener to this class. - * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type, will be called with the fired event. + * Bind the layer groups of the stage. */ - this.addEventListener = function (type, callback) { - listenerHandler.add(type, callback); + this.bindLayerGroups = function () { + if (layerGroups.length === 0 || + layerGroups.length === 1 || + binders.length === 0) { + return; + } + // create callback store + callbackStore = new Array(layerGroups.length); + // add listeners + for (var i = 0; i < layerGroups.length; ++i) { + for (var j = 0; j < binders.length; ++j) { + addEventListeners(i, binders[j]); + } + } }; /** - * Remove an event listener from this class. - * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type. + * Unbind the layer groups of the stage. */ - this.removeEventListener = function (type, callback) { - listenerHandler.remove(type, callback); + this.unbindLayerGroups = function () { + if (layerGroups.length === 0 || + layerGroups.length === 1 || + binders.length === 0 || + !callbackStore) { + return; + } + // remove listeners + for (var i = 0; i < layerGroups.length; ++i) { + for (var j = 0; j < binders.length; ++j) { + removeEventListeners(i, binders[j]); + } + } + // clear callback store + callbackStore = null; }; /** - * Fire an event: call all associated listeners with the input event object. + * Get the binder callback function for a given layer group index. + * The function is created if not yet stored. * - * @param {object} event The event to fire. - * @private + * @param {object} binder The layer binder. + * @param {number} index The index of the associated layer group. + * @returns {Function} The binder function. */ - function fireEvent(event) { - listenerHandler.fireEvent(event); + function getBinderCallback(binder, index) { + if (typeof callbackStore[index] === 'undefined') { + callbackStore[index] = []; + } + var store = callbackStore[index]; + var binderObj = store.find(function (elem) { + return elem.binder === binder; + }); + if (typeof binderObj === 'undefined') { + // create new callback object + binderObj = { + binder: binder, + callback: function (event) { + // stop listeners + removeEventListeners(index, binder); + // apply binder + binder.getCallback(layerGroups[index])(event); + // re-start listeners + addEventListeners(index, binder); + } + }; + callbackStore[index].push(binderObj); + } + return binderObj.callback; } - // common layer methods [end] --------------- - /** - * Update label scale: compensate for it so - * that label size stays visually the same. + * Add event listeners for a given layer group index and binder. * - * @param {object} scale The scale to compensate for + * @param {number} index The index of the associated layer group. + * @param {object} binder The layer binder. */ - function updateLabelScale(scale) { - // same formula as in style::applyZoomScale: - // compensate for scale and times 2 so that font 10 looks like a 10 - var ratioX = 2 / scale.x; - var ratioY = 2 / scale.y; - // compensate scale for labels - var labels = konvaStage.find('Label'); - for (var i = 0; i < labels.length; ++i) { - labels[i].scale({x: ratioX, y: ratioY}); - } - } -}; // DrawLayer class - -// namespaces -var dwv = dwv || {}; -dwv.gui = dwv.gui || {}; -dwv.gui.base = dwv.gui.base || {}; - -/** - * List of interaction event names. - */ -dwv.gui.interactionEventNames = [ - 'mousedown', - 'mousemove', - 'mouseup', - 'mouseout', - 'wheel', - 'dblclick', - 'touchstart', - 'touchmove', - 'touchend' -]; - -/** - * Get a HTML element associated to a container div. - * - * @param {number} containerDivId The id of the container div. - * @param {string} name The name or id to find. - * @returns {object} The found element or null. - */ -dwv.gui.base.getElement = function (containerDivId, name) { - // get by class in the container div - var parent = document.getElementById(containerDivId); - if (!parent) { - return null; - } - var elements = parent.getElementsByClassName(name); - // getting the last element since some libraries (ie jquery-mobile) create - // span in front of regular tags (such as select)... - var element = elements[elements.length - 1]; - // if not found get by id with 'containerDivId-className' - if (typeof element === 'undefined') { - element = document.getElementById(containerDivId + '-' + name); - } - return element; -}; - -/** - * Get the size available for a div. - * - * @param {object} div The input div. - * @returns {object} The available width and height as {x,y}. - */ -dwv.gui.getDivSize = function (div) { - var parent = div.parentNode; - // offsetHeight: height of an element, including vertical padding - // and borders - // ref: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight - var height = parent.offsetHeight; - // remove the height of other elements of the container div - var kids = parent.children; - for (var i = 0; i < kids.length; ++i) { - if (!kids[i].classList.contains(div.className)) { - var styles = window.getComputedStyle(kids[i]); - // offsetHeight does not include margin - var margin = parseFloat(styles.getPropertyValue('margin-top'), 10) + - parseFloat(styles.getPropertyValue('margin-bottom'), 10); - height -= (kids[i].offsetHeight + margin); + function addEventListeners(index, binder) { + for (var i = 0; i < layerGroups.length; ++i) { + if (i !== index) { + layerGroups[index].addEventListener( + binder.getEventType(), + getBinderCallback(binder, i) + ); + } } } - return {x: parent.offsetWidth, y: height}; -}; -/** - * Get the positions (without the parent offset) of a list of touch events. - * - * @param {Array} touches The list of touch events. - * @returns {Array} The list of positions of the touch events. - */ -dwv.gui.getTouchesPositions = function (touches) { - // get the touch offset from all its parents - var offsetLeft = 0; - var offsetTop = 0; - if (touches.length !== 0 && - typeof touches[0].target !== 'undefined') { - var offsetParent = touches[0].target.offsetParent; - while (offsetParent) { - if (!isNaN(offsetParent.offsetLeft)) { - offsetLeft += offsetParent.offsetLeft; - } - if (!isNaN(offsetParent.offsetTop)) { - offsetTop += offsetParent.offsetTop; + /** + * Remove event listeners for a given layer group index and binder. + * + * @param {number} index The index of the associated layer group. + * @param {object} binder The layer binder. + */ + function removeEventListeners(index, binder) { + for (var i = 0; i < layerGroups.length; ++i) { + if (i !== index) { + layerGroups[index].removeEventListener( + binder.getEventType(), + getBinderCallback(binder, i) + ); } - offsetParent = offsetParent.offsetParent; } - } else { - dwv.logger.debug('No touch target offset parent.'); - } - // set its position - var positions = []; - for (var i = 0; i < touches.length; ++i) { - positions.push({ - x: touches[i].pageX - offsetLeft, - y: touches[i].pageY - offsetTop - }); - } - return positions; -}; - -/** - * Get the offset of an input event. - * - * @param {object} event The event to get the offset from. - * @returns {Array} The array of offsets. - */ -dwv.gui.getEventOffset = function (event) { - var positions = []; - if (typeof event.targetTouches !== 'undefined' && - event.targetTouches.length !== 0) { - // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/targetTouches - positions = dwv.gui.getTouchesPositions(event.targetTouches); - } else if (typeof event.changedTouches !== 'undefined' && - event.changedTouches.length !== 0) { - // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/changedTouches - positions = dwv.gui.getTouchesPositions(event.changedTouches); - } else { - // layerX is used by Firefox - var ex = event.offsetX === undefined ? event.layerX : event.offsetX; - var ey = event.offsetY === undefined ? event.layerY : event.offsetY; - positions.push({x: ex, y: ey}); - } - return positions; -}; - -/** - * Test if a canvas with the input size can be created. - * - * @see https://github.com/ivmartel/dwv/issues/902 - * @see https://github.com/jhildenbiddle/canvas-size/blob/v1.2.4/src/canvas-test.js - * @param {number} width The canvas width. - * @param {number} height The canvas height. - * @returns {boolean} True is the canvas can be created. - */ -dwv.gui.canCreateCanvas = function (width, height) { - // test canvas with input size - var testCvs = document.createElement('canvas'); - testCvs.width = width; - testCvs.height = height; - // crop canvas to speed up test - var cropCvs = document.createElement('canvas'); - cropCvs.width = 1; - cropCvs.height = 1; - // contexts - var testCtx = testCvs.getContext('2d'); - var cropCtx = cropCvs.getContext('2d'); - // set data - if (testCtx) { - testCtx.fillRect(width - 1, height - 1, 1, 1); - // Render the test pixel in the bottom-right corner of the - // test canvas in the top-left of the 1x1 crop canvas. This - // dramatically reducing the time for getImageData to complete. - cropCtx.drawImage(testCvs, width - 1, height - 1, 1, 1, 0, 0, 1, 1); } - // Verify image data (alpha component, Pass = 255, Fail = 0) - return cropCtx && cropCtx.getImageData(0, 0, 1, 1).data[3] !== 0; }; // namespaces @@ -12076,7 +12901,8 @@ dwv.gui = dwv.gui || {}; /** * View layer. * - * @param {object} containerDiv The layer div. + * @param {object} containerDiv The layer div, its id will be used + * as this layer id. * @class */ dwv.gui.ViewLayer = function (containerDiv) { @@ -12087,13 +12913,6 @@ dwv.gui.ViewLayer = function (containerDiv) { // closure to self var self = this; - /** - * The image view. - * - * @private - * @type {object} - */ - var view = null; /** * The view controller. * @@ -12103,19 +12922,19 @@ dwv.gui.ViewLayer = function (containerDiv) { var viewController = null; /** - * The base canvas. + * The main display canvas. * * @private * @type {object} */ var canvas = null; /** - * A cache of the initial canvas. + * The offscreen canvas: used to store the raw, unscaled pixel data. * * @private * @type {object} */ - var cacheCanvas = null; + var offscreenCanvas = null; /** * The associated CanvasRenderingContext2D. * @@ -12133,12 +12952,20 @@ dwv.gui.ViewLayer = function (containerDiv) { var imageData = null; /** - * The layer size as {x,y}. + * The layer base size as {x,y}. + * + * @private + * @type {object} + */ + var baseSize; + + /** + * The layer base spacing as {x,y}. * * @private * @type {object} */ - var layerSize; + var baseSpacing; /** * The layer opacity. @@ -12156,6 +12983,14 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var scale = {x: 1, y: 1}; + /** + * The layer fit scale. + * + * @private + * @type {object} + */ + var fitScale = {x: 1, y: 1}; + /** * The layer offset. * @@ -12164,6 +12999,14 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var offset = {x: 0, y: 0}; + /** + * The base layer offset. + * + * @private + * @type {object} + */ + var baseOffset = {x: 0, y: 0}; + /** * Data update flag. * @@ -12180,6 +13023,15 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var dataIndex = null; + /** + * Get the associated data index. + * + * @returns {number} The index. + */ + this.getDataIndex = function () { + return dataIndex; + }; + /** * Listener handler. * @@ -12188,6 +13040,25 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var listenerHandler = new dwv.utils.ListenerHandler(); + /** + * Set the associated view. + * + * @param {object} view The view. + */ + this.setView = function (view) { + // local listeners + view.addEventListener('wlchange', onWLChange); + view.addEventListener('colourchange', onColourChange); + view.addEventListener('positionchange', onPositionChange); + view.addEventListener('alphafuncchange', onAlphaFuncChange); + // view events + for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { + view.addEventListener(dwv.image.viewEventNames[j], fireEvent); + } + // create view controller + viewController = new dwv.ctrl.ViewController(view); + }; + /** * Get the view controller. * @@ -12214,7 +13085,7 @@ dwv.gui.ViewLayer = function (containerDiv) { this.onimagechange = function (event) { // event.value = [index, image] if (dataIndex === event.value[0]) { - view.setImage(event.value[1]); + viewController.setImage(event.value[1]); needsDataUpdate = true; } }; @@ -12222,12 +13093,33 @@ dwv.gui.ViewLayer = function (containerDiv) { // common layer methods [start] --------------- /** - * Get the layer size. + * Get the id of the layer. + * + * @returns {string} The string id. + */ + this.getId = function () { + return containerDiv.id; + }; + + /** + * Get the data full size, ie size * spacing. + * + * @returns {object} The full size as {x,y}. + */ + this.getFullSize = function () { + return { + x: baseSize.x * baseSpacing.x, + y: baseSize.y * baseSpacing.y + }; + }; + + /** + * Get the layer base size (without scale). * * @returns {object} The size as {x,y}. */ - this.getSize = function () { - return layerSize; + this.getBaseSize = function () { + return baseSize; }; /** @@ -12246,6 +13138,19 @@ dwv.gui.ViewLayer = function (containerDiv) { */ this.setOpacity = function (alpha) { opacity = Math.min(Math.max(alpha, 0), 1); + + /** + * Opacity change event. + * + * @event dwv.App#opacitychange + * @type {object} + * @property {string} type The event type. + */ + var event = { + type: 'opacitychange', + value: [opacity] + }; + fireEvent(event); }; /** @@ -12253,8 +13158,29 @@ dwv.gui.ViewLayer = function (containerDiv) { * * @param {object} newScale The scale as {x,y}. */ - this.setScale = function (newScale) { - scale = newScale; + this.setScale = function (newScale) { + var helper = viewController.getPlaneHelper(); + var orientedNewScale = helper.getOrientedXYZ(newScale); + scale = { + x: fitScale.x * orientedNewScale.x, + y: fitScale.y * orientedNewScale.y + }; + }; + + /** + * Set the base layer offset. Resets the layer offset. + * + * @param {object} off The offset as {x,y}. + */ + this.setBaseOffset = function (off) { + var helper = viewController.getPlaneHelper(); + baseOffset = helper.getPlaneOffsetFromOffset3D({ + x: off.getX(), + y: off.getY(), + z: off.getZ() + }); + // reset offset + offset = baseOffset; }; /** @@ -12263,29 +13189,42 @@ dwv.gui.ViewLayer = function (containerDiv) { * @param {object} newOffset The offset as {x,y}. */ this.setOffset = function (newOffset) { - offset = newOffset; + var helper = viewController.getPlaneHelper(); + var planeNewOffset = helper.getPlaneOffsetFromOffset3D(newOffset); + offset = { + x: baseOffset.x + planeNewOffset.x, + y: baseOffset.y + planeNewOffset.y + }; }; /** - * Set the layer z-index. + * Transform a display position to an index. * - * @param {number} index The index. + * @param {number} x The X position. + * @param {number} y The Y position. + * @returns {dwv.math.Index} The equivalent index. */ - this.setZIndex = function (index) { - containerDiv.style.zIndex = index; + this.displayToPlaneIndex = function (x, y) { + var planePos = this.displayToPlanePos(x, y); + return new dwv.math.Index([ + Math.floor(planePos.x), + Math.floor(planePos.y) + ]); }; - /** - * Resize the layer: update the window scale and layer sizes. - * - * @param {object} newScale The layer scale as {x,y}. - */ - this.resize = function (newScale) { - // resize canvas - canvas.width = parseInt(layerSize.x * newScale.x, 10); - canvas.height = parseInt(layerSize.y * newScale.y, 10); - // set scale - this.setScale(newScale); + this.displayToPlaneScale = function (x, y) { + return { + x: x / scale.x, + y: y / scale.y + }; + }; + + this.displayToPlanePos = function (x, y) { + var deScaled = this.displayToPlaneScale(x, y); + return { + x: deScaled.x + offset.x, + y: deScaled.y + offset.y + }; }; /** @@ -12321,7 +13260,10 @@ dwv.gui.ViewLayer = function (containerDiv) { * @type {object} * @property {string} type The event type. */ - var event = {type: 'renderstart'}; + var event = { + type: 'renderstart', + layerid: this.getId() + }; fireEvent(event); // update data if needed @@ -12359,7 +13301,7 @@ dwv.gui.ViewLayer = function (containerDiv) { // disable smoothing (set just before draw, could be reset by resize) context.imageSmoothingEnabled = false; // draw image - context.drawImage(cacheCanvas, 0, 0); + context.drawImage(offscreenCanvas, 0, 0); /** * Render end event. @@ -12368,41 +13310,25 @@ dwv.gui.ViewLayer = function (containerDiv) { * @type {object} * @property {string} type The event type. */ - event = {type: 'renderend'}; + event = { + type: 'renderend', + layerid: this.getId() + }; fireEvent(event); }; /** * Initialise the layer: set the canvas and context * - * @param {object} image The image. - * @param {object} metaData The image meta data. + * @param {object} size The image size as {x,y}. + * @param {object} spacing The image spacing as {x,y}. * @param {number} index The associated data index. */ - this.initialise = function (image, metaData, index) { + this.initialise = function (size, spacing, index) { + // set locals + baseSize = size; + baseSpacing = spacing; dataIndex = index; - // create view - var viewFactory = new dwv.image.ViewFactory(); - view = viewFactory.create( - new dwv.dicom.DicomElementsWrapper(metaData), - image); - - // local listeners - view.addEventListener('wlwidthchange', onWLChange); - view.addEventListener('wlcenterchange', onWLChange); - view.addEventListener('colourchange', onColourChange); - view.addEventListener('slicechange', onSliceChange); - view.addEventListener('framechange', onFrameChange); - - // create view controller - viewController = new dwv.ViewController(view); - - // get sizes - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; // create canvas canvas = document.createElement('canvas'); @@ -12419,25 +13345,55 @@ dwv.gui.ViewLayer = function (containerDiv) { alert('Error: failed to get the 2D context.'); return; } + + // check canvas + if (!dwv.gui.canCreateCanvas(baseSize.x, baseSize.y)) { + throw new Error('Cannot create canvas ' + baseSize.x + ', ' + baseSize.y); + } + // canvas sizes - canvas.width = layerSize.x; - canvas.height = layerSize.y; + canvas.width = baseSize.x; + canvas.height = baseSize.y; + // off screen canvas + offscreenCanvas = document.createElement('canvas'); + offscreenCanvas.width = baseSize.x; + offscreenCanvas.height = baseSize.y; // original empty image data array - context.clearRect(0, 0, canvas.width, canvas.height); - imageData = context.createImageData(canvas.width, canvas.height); - // cached canvas - cacheCanvas = document.createElement('canvas'); - cacheCanvas.width = canvas.width; - cacheCanvas.height = canvas.height; + context.clearRect(0, 0, baseSize.x, baseSize.y); + imageData = context.createImageData(baseSize.x, baseSize.y); // update data on first draw needsDataUpdate = true; }; /** - * Activate the layer: propagate events. + * Fit the layer to its parent container. + * + * @param {number} fitScale1D The 1D fit scale. + */ + this.fitToContainer = function (fitScale1D) { + // update fit scale + fitScale = { + x: fitScale1D * baseSpacing.x, + y: fitScale1D * baseSpacing.y + }; + // update canvas + var fullSize = this.getFullSize(); + var width = Math.floor(fullSize.x * fitScale1D); + var height = Math.floor(fullSize.y * fitScale1D); + if (!dwv.gui.canCreateCanvas(width, height)) { + throw new Error('Cannot resize canvas ' + width + ', ' + height); + } + canvas.width = width; + canvas.height = height; + // reset scale + this.setScale({x: 1, y: 1, z: 1}); + }; + + /** + * Enable and listen to container interaction events. */ - this.activate = function () { + this.bindInteraction = function () { // allow pointer events containerDiv.style.pointerEvents = 'auto'; // interaction events @@ -12448,9 +13404,9 @@ dwv.gui.ViewLayer = function (containerDiv) { }; /** - * Deactivate the layer: stop propagating events. + * Disable and stop listening to container interaction events. */ - this.deactivate = function () { + this.unbindInteraction = function () { // disable pointer events containerDiv.style.pointerEvents = 'none'; // interaction events @@ -12489,35 +13445,21 @@ dwv.gui.ViewLayer = function (containerDiv) { * @private */ function fireEvent(event) { + event.srclayerid = self.getId(); + event.dataindex = dataIndex; listenerHandler.fireEvent(event); } // common layer methods [end] --------------- - /** - * Propagate (or not) view events. - * - * @param {boolean} flag True to propagate. - */ - this.propagateViewEvents = function (flag) { - // view events - for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - if (flag) { - view.addEventListener(dwv.image.viewEventNames[j], fireEvent); - } else { - view.removeEventListener(dwv.image.viewEventNames[j], fireEvent); - } - } - }; - /** * Update the canvas image data. */ function updateImageData() { // generate image data - view.generateImageData(imageData); - // pass the data to the canvas - cacheCanvas.getContext('2d').putImageData(imageData, 0, 0); + viewController.generateImageData(imageData); + // pass the data to the off screen canvas + offscreenCanvas.getContext('2d').putImageData(imageData, 0, 0); // update data flag needsDataUpdate = false; } @@ -12549,40 +13491,53 @@ dwv.gui.ViewLayer = function (containerDiv) { } /** - * Handle frame change. + * Handle position change. * - * @param {object} event The event fired when changing the frame. + * @param {object} event The event fired when changing the position. * @private */ - function onFrameChange(event) { - // generate and draw if no skip flag + function onPositionChange(event) { if (typeof event.skipGenerate === 'undefined' || event.skipGenerate === false) { - needsDataUpdate = true; - self.draw(); + // 3D dimensions + var dims3D = [0, 1, 2]; + // remove scroll index + var indexScrollIndex = dims3D.indexOf(viewController.getScrollIndex()); + dims3D.splice(indexScrollIndex, 1); + // remove non scroll index from diff dims + var diffDims = event.diffDims.filter(function (item) { + return dims3D.indexOf(item) === -1; + }); + // update if we have something left + if (diffDims.length !== 0) { + needsDataUpdate = true; + self.draw(); + } } } /** - * Handle slice change. + * Handle alpha function change. * - * @param {object} _event The event fired when changing the slice. + * @param {object} event The event fired when changing the function. * @private */ - function onSliceChange(_event) { - needsDataUpdate = true; - self.draw(); + function onAlphaFuncChange(event) { + if (typeof event.skipGenerate === 'undefined' || + event.skipGenerate === false) { + needsDataUpdate = true; + self.draw(); + } } /** - * Update the layer position. + * Set the current position. * - * @param {object} pos The new position. + * @param {dwv.math.Point} position The new position. + * @param {dwv.math.Index} _index The new index. */ - this.updatePosition = function (pos) { - viewController.setCurrentPosition(pos[0]); - viewController.setCurrentFrame(pos[1]); - needsDataUpdate = true; + this.setCurrentPosition = function (position, _index) { + viewController.setCurrentPosition(position); }; /** @@ -12648,27 +13603,29 @@ var hasJpeg2000Decoder = (typeof JpxImage !== 'undefined'); */ dwv.image.AsynchPixelBufferDecoder = function (script, _numberOfData) { // initialise the thread pool - var pool = new dwv.utils.ThreadPool(15); + var pool = new dwv.utils.ThreadPool(10); // flag to know if callbacks are set var areCallbacksSet = false; + // closure to self + var self = this; /** * Decode a pixel buffer. * * @param {Array} pixelBuffer The pixel buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { if (!areCallbacksSet) { areCallbacksSet = true; // set event handlers - pool.onworkstart = this.ondecodestart; - pool.onworkitem = this.ondecodeditem; - pool.onwork = this.ondecoded; - pool.onworkend = this.ondecodeend; - pool.onerror = this.onerror; - pool.onabort = this.onabort; + pool.onworkstart = self.ondecodestart; + pool.onworkitem = self.ondecodeditem; + pool.onwork = self.ondecoded; + pool.onworkend = self.ondecodeend; + pool.onerror = self.onerror; + pool.onabort = self.onabort; } // create worker task var workerTask = new dwv.utils.WorkerTask( @@ -12677,7 +13634,7 @@ dwv.image.AsynchPixelBufferDecoder = function (script, _numberOfData) { buffer: pixelBuffer, meta: pixelMeta }, - index + info ); // add it the queue and run it pool.addWorkerTask(workerTask); @@ -12758,12 +13715,12 @@ dwv.image.SynchPixelBufferDecoder = function (algoName, numberOfData) { * * @param {Array} pixelBuffer The pixel buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. * @external jpeg * @external JpegImage * @external JpxImage */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { ++decodeCount; var decoder = null; @@ -12821,7 +13778,7 @@ dwv.image.SynchPixelBufferDecoder = function (algoName, numberOfData) { // send decode events this.ondecodeditem({ data: [decodedBuffer], - index: index + index: info.itemNumber }); // decode end? if (decodeCount === numberOfData) { @@ -12912,7 +13869,7 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { // initialise the asynch decoder (if possible) if (typeof dwv.image.decoderScripts !== 'undefined' && - typeof dwv.image.decoderScripts[algoName] !== 'undefined') { + typeof dwv.image.decoderScripts[algoName] !== 'undefined') { pixelDecoder = new dwv.image.AsynchPixelBufferDecoder( dwv.image.decoderScripts[algoName], numberOfData); } else { @@ -12928,9 +13885,9 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { * * @param {Array} pixelBuffer The input data buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { if (!areCallbacksSet) { areCallbacksSet = true; // set callbacks @@ -12942,7 +13899,7 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { pixelDecoder.onabort = this.onabort; } // decode and call the callback - pixelDecoder.decode(pixelBuffer, pixelMeta, index); + pixelDecoder.decode(pixelBuffer, pixelMeta, info); }; /** @@ -13014,20 +13971,20 @@ dwv.image.DicomBufferToView = function () { var self = this; /** - * The default character set (optional). + * Converter options. * * @private - * @type {string} + * @type {object} */ - var defaultCharacterSet; + var options; /** - * Set the default character set. + * Set the converter options. * - * @param {string} characterSet The character set. + * @param {object} opt The input options. */ - this.setDefaultCharacterSet = function (characterSet) { - defaultCharacterSet = characterSet; + this.setOptions = function (opt) { + options = opt; }; /** @@ -13039,6 +13996,110 @@ dwv.image.DicomBufferToView = function () { */ var pixelDecoder = null; + // local tmp storage + var dicomParserStore = []; + var finalBufferStore = []; + var decompressedSizes = []; + + /** + * Generate the image object. + * + * @param {number} index The data index. + * @param {string} origin The data origin. + */ + function generateImage(index, origin) { + // create the image + try { + var imageFactory = new dwv.ImageFactory(); + var image = imageFactory.create( + dicomParserStore[index].getDicomElements(), + finalBufferStore[index], + options.numberOfFiles); + // call onloaditem + self.onloaditem({ + data: { + image: image, + info: dicomParserStore[index].getRawDicomElements() + }, + source: origin + }); + } catch (error) { + self.onerror({ + error: error, + source: origin + }); + self.onloadend({ + source: origin + }); + } + } + + /** + * Handle a decoded item event. + * + * @param {object} event The decoded item event. + */ + function onDecodedItem(event) { + // send progress + self.onprogress({ + lengthComputable: true, + loaded: event.itemNumber + 1, + total: event.numberOfItems, + index: event.dataIndex, + source: origin + }); + + var dataIndex = event.dataIndex; + + // store decoded data + var decodedData = event.data[0]; + if (event.numberOfItems !== 1) { + // allocate buffer if not done yet + if (typeof decompressedSizes[dataIndex] === 'undefined') { + decompressedSizes[dataIndex] = decodedData.length; + var fullSize = event.numberOfItems * decompressedSizes[dataIndex]; + try { + finalBufferStore[dataIndex] = new decodedData.constructor(fullSize); + } catch (error) { + if (error instanceof RangeError) { + var powerOf2 = Math.floor(Math.log(fullSize) / Math.log(2)); + dwv.logger.error('Cannot allocate ' + + decodedData.constructor.name + + ' of size: ' + + fullSize + ' (>2^' + powerOf2 + ') for decompressed data.'); + } + // abort + pixelDecoder.abort(); + // send events + self.onerror({ + error: error, + source: origin + }); + self.onloadend({ + source: origin + }); + // exit + return; + } + } + // hoping for all items to have the same size... + if (decodedData.length !== decompressedSizes[dataIndex]) { + dwv.logger.warn('Unsupported varying decompressed data size: ' + + decodedData.length + ' != ' + decompressedSizes[dataIndex]); + } + // set buffer item data + finalBufferStore[dataIndex].set( + decodedData, decompressedSizes[dataIndex] * event.itemNumber); + } else { + finalBufferStore[dataIndex] = decodedData; + } + + // create image for the first item + if (event.itemNumber === 0) { + generateImage(dataIndex, origin); + } + } + /** * Get data from an input buffer using a DICOM parser. * @@ -13048,15 +14109,22 @@ dwv.image.DicomBufferToView = function () { */ this.convert = function (buffer, origin, dataIndex) { self.onloadstart({ - source: origin + source: origin, + dataIndex: dataIndex }); // DICOM parser var dicomParser = new dwv.dicom.DicomParser(); - dicomParser.setDefaultCharacterSet(defaultCharacterSet); + var imageFactory = new dwv.ImageFactory(); + + if (typeof options.defaultCharacterSet !== 'undefined') { + dicomParser.setDefaultCharacterSet(options.defaultCharacterSet); + } // parse the buffer try { dicomParser.parse(buffer); + // check elements are good for image + imageFactory.checkElements(dicomParser.getDicomElements()); } catch (error) { self.onerror({ error: error, @@ -13069,36 +14137,16 @@ dwv.image.DicomBufferToView = function () { } var pixelBuffer = dicomParser.getRawDicomElements().x7FE00010.value; + // help GC: discard pixel buffer from elements + dicomParser.getRawDicomElements().x7FE00010.value = []; var syntax = dwv.dicom.cleanString( dicomParser.getRawDicomElements().x00020010.value[0]); var algoName = dwv.dicom.getSyntaxDecompressionName(syntax); var needDecompression = (algoName !== null); - // generate the image - var generateImage = function (/*event*/) { - // create the image - var imageFactory = new dwv.image.ImageFactory(); - try { - var image = imageFactory.create( - dicomParser.getDicomElements(), pixelBuffer); - // call onload - self.onloaditem({ - data: { - image: image, - info: dicomParser.getRawDicomElements() - }, - source: origin - }); - } catch (error) { - self.onerror({ - error: error, - source: origin - }); - self.onloadend({ - source: origin - }); - } - }; + // store + dicomParserStore[dataIndex] = dicomParser; + finalBufferStore[dataIndex] = pixelBuffer[0]; if (needDecompression) { // gather pixel buffer meta data @@ -13125,45 +14173,38 @@ dwv.image.DicomBufferToView = function () { pixelMeta.planarConfiguration = planarConfigurationElement.value[0]; } - // number of frames - var numberOfFrames = pixelBuffer.length; - - // decoder callback - var countDecodedFrames = 0; - var onDecodedFrame = function (event) { - ++countDecodedFrames; - // send progress - self.onprogress({ - lengthComputable: true, - loaded: (countDecodedFrames * 100 / numberOfFrames), - total: 100, - index: dataIndex, - source: origin - }); - // store data - var frameNb = event.index; - pixelBuffer[frameNb] = event.data[0]; - // create image for the first frame - if (frameNb === 0) { - generateImage(); - } - }; - - // setup the decoder (one decoder per convert) - // TODO check if it is ok to create a worker pool per file... - pixelDecoder = new dwv.image.PixelBufferDecoder( - algoName, numberOfFrames); - // callbacks - // pixelDecoder.ondecodestart: nothing to do - pixelDecoder.ondecodeditem = onDecodedFrame; - pixelDecoder.ondecoded = self.onload; - pixelDecoder.ondecodeend = self.onloadend; - pixelDecoder.onerror = self.onerror; - pixelDecoder.onabort = self.onabort; + // number of items + var numberOfItems = pixelBuffer.length; + + // setup the decoder (one decoder per all converts) + if (pixelDecoder === null) { + pixelDecoder = new dwv.image.PixelBufferDecoder( + algoName, numberOfItems); + // callbacks + // pixelDecoder.ondecodestart: nothing to do + pixelDecoder.ondecodeditem = function (event) { + onDecodedItem(event); + // send onload and onloadend when all items have been decoded + if (event.itemNumber + 1 === event.numberOfItems) { + self.onload(event); + self.onloadend(event); + } + }; + // pixelDecoder.ondecoded: nothing to do + // pixelDecoder.ondecodeend: nothing to do + pixelDecoder.onerror = self.onerror; + pixelDecoder.onabort = self.onabort; + } // launch decode - for (var f = 0; f < numberOfFrames; ++f) { - pixelDecoder.decode(pixelBuffer[f], pixelMeta, f); + for (var i = 0; i < numberOfItems; ++i) { + pixelDecoder.decode(pixelBuffer[i], pixelMeta, + { + itemNumber: i, + numberOfItems: numberOfItems, + dataIndex: dataIndex + } + ); } } else { // no decompression @@ -13176,7 +14217,7 @@ dwv.image.DicomBufferToView = function () { source: origin }); // generate image - generateImage(); + generateImage(dataIndex, origin); // send load events self.onload({ source: origin @@ -13205,6 +14246,13 @@ dwv.image.DicomBufferToView = function () { * @param {object} _event The load start event. */ dwv.image.DicomBufferToView.prototype.onloadstart = function (_event) {}; +/** + * Handle a load item event. + * Default does nothing. + * + * @param {object} _event The load item event. + */ +dwv.image.DicomBufferToView.prototype.onloaditem = function (_event) {}; /** * Handle a load progress event. * Default does nothing. @@ -13284,20 +14332,22 @@ dwv.image.getDefaultImage = function ( imageBuffer, numberOfFrames, imageUid) { // image size - var imageSize = new dwv.image.Size(width, height); + var imageSize = new dwv.image.Size([width, height, 1]); // default spacing // TODO: misleading... - var imageSpacing = new dwv.image.Spacing(1, 1); + var imageSpacing = new dwv.image.Spacing([1, 1, 1]); // default origin var origin = new dwv.math.Point3D(0, 0, sliceIndex); // create image var geometry = new dwv.image.Geometry(origin, imageSize, imageSpacing); - var image = new dwv.image.Image( - geometry, imageBuffer, numberOfFrames, [imageUid]); + var image = new dwv.image.Image(geometry, imageBuffer, [imageUid]); image.setPhotometricInterpretation('RGB'); // meta information var meta = {}; meta.BitsStored = 8; + if (typeof numberOfFrames !== 'undefined') { + meta.numberOfFiles = numberOfFrames; + } image.setMeta(meta); // return return image; @@ -13342,7 +14392,7 @@ dwv.image.getViewFromDOMImage = function (domImage, origin) { // create view var imageBuffer = dwv.image.imageDataToBuffer(imageData); var image = dwv.image.getDefaultImage( - width, height, sliceIndex, [imageBuffer], 1, sliceIndex); + width, height, sliceIndex, imageBuffer, 1, sliceIndex); // return return { @@ -13375,7 +14425,7 @@ dwv.image.getViewFromDOMVideo = function ( // default frame rate... var frameRate = 30; // number of frames - var numberOfFrames = Math.floor(video.duration * frameRate); + var numberOfFrames = Math.ceil(video.duration * frameRate); // video properties var info = {}; @@ -13387,6 +14437,7 @@ dwv.image.getViewFromDOMVideo = function ( info['imageWidth'] = {value: width}; info['imageHeight'] = {value: height}; info['numberOfFrames'] = {value: numberOfFrames}; + info['imageUid'] = {value: 0}; // draw the image in the canvas in order to get its data var canvas = document.createElement('canvas'); @@ -13422,7 +14473,7 @@ dwv.image.getViewFromDOMVideo = function ( if (frameIndex === 0) { // create view image = dwv.image.getDefaultImage( - width, height, 1, [imgBuffer], numberOfFrames, dataIndex); + width, height, 1, imgBuffer, numberOfFrames, dataIndex); // call callback onloaditem({ data: { @@ -13432,7 +14483,7 @@ dwv.image.getViewFromDOMVideo = function ( source: origin }); } else { - image.appendFrameBuffer(imgBuffer); + image.appendFrameBuffer(imgBuffer, frameIndex); } // increment index ++frameIndex; @@ -13539,13 +14590,13 @@ dwv.image.filter.Threshold = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -13553,7 +14604,7 @@ dwv.image.filter.Threshold = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -13563,7 +14614,7 @@ dwv.image.filter.Threshold = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Threshold.prototype.update = function () { var image = this.getOriginalImage(); @@ -13597,13 +14648,13 @@ dwv.image.filter.Sharpen = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -13611,7 +14662,7 @@ dwv.image.filter.Sharpen = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -13621,7 +14672,7 @@ dwv.image.filter.Sharpen = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Sharpen.prototype.update = function () { var image = this.getOriginalImage(); @@ -13656,13 +14707,13 @@ dwv.image.filter.Sobel = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -13670,7 +14721,7 @@ dwv.image.filter.Sobel = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -13680,7 +14731,7 @@ dwv.image.filter.Sobel = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Sobel.prototype.update = function () { var image = this.getOriginalImage(); @@ -13704,180 +14755,26 @@ dwv.image.filter.Sobel.prototype.update = function () { 0, 0, -1, - -2, - -1]); - - return gradX.compose(gradY, function (x, y) { - return Math.sqrt(x * x + y * y); - }); -}; - -// namespaces -var dwv = dwv || {}; -dwv.image = dwv.image || {}; - -/** - * 2D/3D Size class. - * - * @class - * @param {number} numberOfColumns The number of columns. - * @param {number} numberOfRows The number of rows. - * @param {number} numberOfSlices The number of slices. - */ -dwv.image.Size = function (numberOfColumns, numberOfRows, numberOfSlices) { - /** - * Get the number of columns. - * - * @returns {number} The number of columns. - */ - this.getNumberOfColumns = function () { - return numberOfColumns; - }; - /** - * Get the number of rows. - * - * @returns {number} The number of rows. - */ - this.getNumberOfRows = function () { - return numberOfRows; - }; - /** - * Get the number of slices. - * - * @returns {number} The number of slices. - */ - this.getNumberOfSlices = function () { - return (numberOfSlices || 1.0); - }; -}; - -/** - * Get the size of a slice. - * - * @returns {number} The size of a slice. - */ -dwv.image.Size.prototype.getSliceSize = function () { - return this.getNumberOfColumns() * this.getNumberOfRows(); -}; - -/** - * Get the total size. - * - * @returns {number} The total size. - */ -dwv.image.Size.prototype.getTotalSize = function () { - return this.getSliceSize() * this.getNumberOfSlices(); -}; - -/** - * Check for equality. - * - * @param {dwv.image.Size} rhs The object to compare to. - * @returns {boolean} True if both objects are equal. - */ -dwv.image.Size.prototype.equals = function (rhs) { - return rhs !== null && - this.getNumberOfColumns() === rhs.getNumberOfColumns() && - this.getNumberOfRows() === rhs.getNumberOfRows() && - this.getNumberOfSlices() === rhs.getNumberOfSlices(); -}; - -/** - * Check that coordinates are within bounds. - * - * @param {number} i The column coordinate. - * @param {number} j The row coordinate. - * @param {number} k The slice coordinate. - * @returns {boolean} True if the given coordinates are within bounds. - */ -dwv.image.Size.prototype.isInBounds = function (i, j, k) { - if (i < 0 || i > this.getNumberOfColumns() - 1 || - j < 0 || j > this.getNumberOfRows() - 1 || - k < 0 || k > this.getNumberOfSlices() - 1) { - return false; - } - return true; -}; - -/** - * Get a string representation of the Vector3D. - * - * @returns {string} The vector as a string. - */ -dwv.image.Size.prototype.toString = function () { - return '(' + this.getNumberOfColumns() + - ', ' + this.getNumberOfRows() + - ', ' + this.getNumberOfSlices() + ')'; -}; - -/** - * 2D/3D Spacing class. - * - * @class - * @param {number} columnSpacing The column spacing. - * @param {number} rowSpacing The row spacing. - * @param {number} sliceSpacing The slice spacing. - */ -dwv.image.Spacing = function (columnSpacing, rowSpacing, sliceSpacing) { - /** - * Get the column spacing. - * - * @returns {number} The column spacing. - */ - this.getColumnSpacing = function () { - return columnSpacing; - }; - /** - * Get the row spacing. - * - * @returns {number} The row spacing. - */ - this.getRowSpacing = function () { - return rowSpacing; - }; - /** - * Get the slice spacing. - * - * @returns {number} The slice spacing. - */ - this.getSliceSpacing = function () { - return (sliceSpacing || 1.0); - }; -}; - -/** - * Check for equality. - * - * @param {dwv.image.Spacing} rhs The object to compare to. - * @returns {boolean} True if both objects are equal. - */ -dwv.image.Spacing.prototype.equals = function (rhs) { - return rhs !== null && - this.getColumnSpacing() === rhs.getColumnSpacing() && - this.getRowSpacing() === rhs.getRowSpacing() && - this.getSliceSpacing() === rhs.getSliceSpacing(); -}; + -2, + -1]); -/** - * Get a string representation of the Vector3D. - * - * @returns {string} The vector as a string. - */ -dwv.image.Spacing.prototype.toString = function () { - return '(' + this.getColumnSpacing() + - ', ' + this.getRowSpacing() + - ', ' + this.getSliceSpacing() + ')'; + return gradX.compose(gradY, function (x, y) { + return Math.sqrt(x * x + y * y); + }); }; +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; /** * 2D/3D Geometry class. * * @class - * @param {object} origin The object origin (a 3D point). - * @param {object} size The object size. - * @param {object} spacing The object spacing. - * @param {object} orientation The object orientation (3*3 matrix, + * @param {dwv.math.Point3D} origin The object origin (a 3D point). + * @param {dwv.image.Size} size The object size. + * @param {dwv.image.Spacing} spacing The object spacing. + * @param {dwv.math.Matrix33} orientation The object orientation (3*3 matrix, * default to 3*3 identity). */ dwv.image.Geometry = function (origin, size, spacing, orientation) { @@ -13890,14 +14787,17 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { if (typeof orientation === 'undefined') { orientation = new dwv.math.getIdentityMat33(); } + // flag to know if new origins were added + var newOrigins = false; /** - * Get the object first origin. + * Get the object origin. + * This should be the lowest origin to ease calculations (?). * - * @returns {object} The object first origin. + * @returns {dwv.math.Point3D} The object origin. */ this.getOrigin = function () { - return origin; + return origins[origins.length - 1]; }; /** * Get the object origins. @@ -13909,24 +14809,108 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { }; /** * Get the object size. - * - * @returns {object} The object size. - */ - this.getSize = function () { - return size; + * Warning: the size comes as stored in DICOM, meaning that it could + * be oriented. + * + * @param {dwv.math.Matrix33} viewOrientation The view orientation (optional) + * @returns {dwv.image.Size} The object size. + */ + this.getSize = function (viewOrientation) { + var res = size; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var values = dwv.math.getOrientedArray3D( + [ + size.get(0), + size.get(1), + size.get(2) + ], + viewOrientation); + res = new dwv.image.Size(values); + } + return res; }; + /** - * Get the object spacing. + * Get the slice spacing from the difference in the Z directions + * of the origins. * - * @returns {object} The object spacing. + * @returns {number} The spacing. */ - this.getSpacing = function () { + this.getSliceGeometrySpacing = function () { + if (origins.length === 1) { + return 1; + } + var spacing = null; + // (x, y, z) = orientationMatrix * (i, j, k) + // -> inv(orientationMatrix) * (x, y, z) = (i, j, k) + // applied on the patient position, reorders indices + // so that Z is the slice direction + var orientation2 = orientation.getInverse().asOneAndZeros(); + var deltas = []; + for (var i = 0; i < origins.length - 1; ++i) { + var origin1 = orientation2.multiplyVector3D(origins[i]); + var origin2 = orientation2.multiplyVector3D(origins[i + 1]); + var diff = Math.abs(origin1.getZ() - origin2.getZ()); + if (diff === 0) { + throw new Error('Zero slice spacing.' + + origin1.toString() + ' ' + origin2.toString()); + } + if (spacing === null) { + spacing = diff; + } else { + if (!dwv.math.isSimilar(spacing, diff, dwv.math.BIG_EPSILON)) { + deltas.push(Math.abs(spacing - diff)); + } + } + } + // warn if non constant + if (deltas.length !== 0) { + var sumReducer = function (sum, value) { + return sum + value; + }; + var mean = deltas.reduce(sumReducer) / deltas.length; + if (mean > 1e-4) { + dwv.logger.warn('Varying slice spacing, mean delta: ' + + mean.toFixed(3) + ' (' + deltas.length + ' case(s))'); + } + } return spacing; }; + + /** + * Get the object spacing. + * Warning: the size comes as stored in DICOM, meaning that it could + * be oriented. + * + * @param {dwv.math.Matrix33} viewOrientation The view orientation (optional) + * @returns {dwv.image.Spacing} The object spacing. + */ + this.getSpacing = function (viewOrientation) { + // update slice spacing after appendSlice + if (newOrigins) { + var values = spacing.getValues(); + values[2] = this.getSliceGeometrySpacing(); + spacing = new dwv.image.Spacing(values); + newOrigins = false; + } + var res = spacing; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var orientedValues = dwv.math.getOrientedArray3D( + [ + spacing.get(0), + spacing.get(1), + spacing.get(2) + ], + viewOrientation); + res = new dwv.image.Spacing(orientedValues); + } + return res; + }; + /** * Get the object orientation. * - * @returns {object} The object orientation. + * @returns {dwv.math.Matrix33} The object orientation. */ this.getOrientation = function () { return orientation; @@ -13934,8 +14918,14 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { /** * Get the slice position of a point in the current slice layout. - * - * @param {object} point The point to evaluate. + * Slice indices increase with decreasing origins (high index -> low origin), + * this simplified the handling of reconstruction since it means + * the displayed data is in the same 'direction' as the extracted data. + * As seen in the getOrigin method, the main origin is the lowest one. + * This implies that the index to world and reverse method do some flipping + * magic... + * + * @param {dwv.math.Point3D} point The point to evaluate. * @returns {number} The slice index. */ this.getSliceIndex = function (point) { @@ -13953,36 +14943,64 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { closestSliceIndex = i; } } - // we have the closest point, are we before or after + var closestOrigin = origins[closestSliceIndex]; + // direction between the input point and the closest origin + var pointDir = point.minus(closestOrigin); + // use third orientation matrix column as base plane vector var normal = new dwv.math.Vector3D( orientation.get(2, 0), orientation.get(2, 1), orientation.get(2, 2)); - var dotProd = normal.dotProduct(point.minus(origins[closestSliceIndex])); - var sliceIndex = (dotProd > 0) ? closestSliceIndex + 1 : closestSliceIndex; + // a.dot(b) = ||a|| * ||b|| * cos(theta) + // (https://en.wikipedia.org/wiki/Dot_product#Geometric_definition) + // -> the sign of the dot product depends on the cosinus of + // the angle between the vectors + // -> >0 => vectors are codirectional + // -> <0 => vectors are oposite + var dotProd = normal.dotProduct(pointDir); + // oposite vectors get higher index + var sliceIndex = (dotProd < 0) ? closestSliceIndex + 1 : closestSliceIndex; return sliceIndex; }; /** * Append an origin to the geometry. * - * @param {object} origin The origin to append. + * @param {dwv.math.Point3D} origin The origin to append. * @param {number} index The index at which to append. */ this.appendOrigin = function (origin, index) { + newOrigins = true; // add in origin array origins.splice(index, 0, origin); - // increment slice number - size = new dwv.image.Size( - size.getNumberOfColumns(), - size.getNumberOfRows(), - size.getNumberOfSlices() + 1); + // increment second dimension + var values = size.getValues(); + values[2] += 1; + size = new dwv.image.Size(values); + }; + + /** + * Append a frame to the geometry. + * + */ + this.appendFrame = function () { + // increment third dimension + var sizeValues = size.getValues(); + var spacingValues = spacing.getValues(); + if (sizeValues.length === 4) { + sizeValues[3] += 1; + } else { + sizeValues.push(2); + spacingValues.push(1); + } + size = new dwv.image.Size(sizeValues); + spacing = new dwv.image.Spacing(spacingValues); }; }; /** - * Get a string representation of the Vector3D. + * Get a string representation of the geometry. * - * @returns {string} The vector as a string. + * @returns {string} The geometry as a string. */ dwv.image.Geometry.prototype.toString = function () { return 'Origin: ' + this.getOrigin() + @@ -14004,46 +15022,115 @@ dwv.image.Geometry.prototype.equals = function (rhs) { }; /** - * Convert an index to an offset in memory. + * Check that a point is within bounds. * - * @param {object} index The index to convert. - * @returns {number} The offset + * @param {dwv.math.Point} point The point to check. + * @returns {boolean} True if the given coordinates are within bounds. */ -dwv.image.Geometry.prototype.indexToOffset = function (index) { - var size = this.getSize(); - return index.getI() + - index.getJ() * size.getNumberOfColumns() + - index.getK() * size.getSliceSize(); +dwv.image.Geometry.prototype.isInBounds = function (point) { + // get the corresponding index + var index = this.worldToIndex(point); + return this.getSize().isInBounds(index); }; +/** + * Flip the K index. + * + * @param {dwv.image.Size} size The image size. + * @param {number} k The index. + * @returns {number} The flipped index. + */ +function flipK(size, k) { + return (size.get(2) - 1) - k; +} + /** * Convert an index into world coordinates. * - * @param {object} index The index to convert. - * @returns {dwv.image.Point3D} The corresponding point. + * @param {dwv.math.Index} index The index to convert. + * @returns {dwv.math.Point} The corresponding point. */ dwv.image.Geometry.prototype.indexToWorld = function (index) { + // flip K index (because of the slice order given by getSliceIndex) + var k = flipK(this.getSize(), index.get(2)); + // apply spacing + // (spacing is oriented, apply before orientation) + var spacing = this.getSpacing(); + var orientedPoint3D = new dwv.math.Point3D( + index.get(0) * spacing.get(0), + index.get(1) * spacing.get(1), + k * spacing.get(2) + ); + // de-orient + var point3D = this.getOrientation().multiplyPoint3D(orientedPoint3D); + // keep >3d values + var values = index.getValues(); var origin = this.getOrigin(); + values[0] = origin.getX() + point3D.getX(); + values[1] = origin.getY() + point3D.getY(); + values[2] = origin.getZ() + point3D.getZ(); + // return point + return new dwv.math.Point(values); +}; + +/** + * Convert a 3D point into world coordinates. + * + * @param {dwv.math.Point3D} point The 3D point to convert. + * @returns {dwv.math.Point3D} The corresponding world 3D point. + */ +dwv.image.Geometry.prototype.pointToWorld = function (point) { + // flip K index (because of the slice order given by getSliceIndex) + var k = flipK(this.getSize(), point.getZ()); + // apply spacing + // (spacing is oriented, apply before orientation) var spacing = this.getSpacing(); + var orientedPoint3D = new dwv.math.Point3D( + point.getX() * spacing.get(0), + point.getY() * spacing.get(1), + k * spacing.get(2) + ); + // de-orient + var point3D = this.getOrientation().multiplyPoint3D(orientedPoint3D); + // return point3D + var origin = this.getOrigin(); return new dwv.math.Point3D( - origin.getX() + index.getI() * spacing.getColumnSpacing(), - origin.getY() + index.getJ() * spacing.getRowSpacing(), - origin.getZ() + index.getK() * spacing.getSliceSpacing()); + origin.getX() + point3D.getX(), + origin.getY() + point3D.getY(), + origin.getZ() + point3D.getZ() + ); }; /** * Convert world coordinates into an index. * - * @param {object} point The point to convert. - * @returns {dwv.image.Index} The corresponding index. + * @param {dwv.math.Point} point The point to convert. + * @returns {dwv.math.Index} The corresponding index. */ dwv.image.Geometry.prototype.worldToIndex = function (point) { + // compensate for origin + // (origin is not oriented, compensate before orientation) var origin = this.getOrigin(); + var point3D = new dwv.math.Point3D( + point.get(0) - origin.getX(), + point.get(1) - origin.getY(), + point.get(2) - origin.getZ() + ); + // orient + var orientedPoint3D = + this.getOrientation().getInverse().multiplyPoint3D(point3D); + // keep >3d values + var values = point.getValues(); + // apply spacing and round var spacing = this.getSpacing(); - return new dwv.math.Point3D( - point.getX() / spacing.getColumnSpacing() - origin.getX(), - point.getY() / spacing.getRowSpacing() - origin.getY(), - point.getZ() / spacing.getSliceSpacing() - origin.getZ()); + values[0] = Math.round(orientedPoint3D.getX() / spacing.get(0)); + values[1] = Math.round(orientedPoint3D.getY() / spacing.get(1)); + // flip K index (because of the slice order given by getSliceIndex) + values[2] = flipK(this.getSize(), + Math.round(orientedPoint3D.getZ() / spacing.get(2)) + ); + // return index + return new dwv.math.Index(values); }; // namespaces @@ -14059,39 +15146,26 @@ dwv.image = dwv.image || {}; * - planar configuration (default RGBRGB...). * * @class - * @param {object} geometry The geometry of the image. - * @param {Array} buffer The image data as an array of frame buffers. - * @param {number} numberOfFrames The number of frames (optional, can be used - to anticipate the final number after appends). + * @param {dwv.image.Geometry} geometry The geometry of the image. + * @param {Array} buffer The image data as a one dimensional buffer. * @param {Array} imageUids An array of Uids indexed to slice number. */ -dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { - // use buffer length in not specified - if (typeof numberOfFrames === 'undefined') { - numberOfFrames = buffer.length; - } +dwv.image.Image = function (geometry, buffer, imageUids) { /** - * Get the number of frames. + * Constant rescale slope and intercept (default). * - * @returns {number} The number of frames. + * @private + * @type {object} */ - this.getNumberOfFrames = function () { - return numberOfFrames; - }; - + var rsi = new dwv.image.RescaleSlopeAndIntercept(1, 0); /** - * Rescale slope and intercept. + * Varying rescale slope and intercept. * * @private - * @type {number} + * @type {Array} */ - var rsis = []; - // initialise RSIs - for (var s = 0, nslices = geometry.getSize().getNumberOfSlices(); - s < nslices; ++s) { - rsis.push(new dwv.image.RescaleSlopeAndIntercept(1, 0)); - } + var rsis = null; /** * Flag to know if the RSIs are all identity (1,0). * @@ -14121,31 +15195,20 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { * @type {number} */ var planarConfiguration = 0; - - /** - * Check if the input element is not null. - * - * @param {object} element The element to test. - * @returns {boolean} True if the input is not null. - */ - var isNotNull = function (element) { - return element !== null; - }; - /** * Number of components. * * @private * @type {number} */ - var numberOfComponents = buffer.find(isNotNull).length / ( + var numberOfComponents = buffer.length / ( geometry.getSize().getTotalSize()); - /** - * Meta information. - * - * @private - * @type {object} - */ + /** + * Meta information. + * + * @private + * @type {object} + */ var meta = {}; /** @@ -14171,18 +15234,31 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { var histogram = null; /** - * Get the image UIDs indexed by slice number. + * Listener handler. + * + * @private + * @type {object} + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Get the image UID at a given index. * - * @returns {Array} The UIDs array. + * @param {dwv.math.Index} index The index at which to get the id. + * @returns {string} The UID. */ - this.getImageUids = function () { - return imageUids; + this.getImageUid = function (index) { + var uid = imageUids[0]; + if (imageUids.length !== 1 && typeof index !== 'undefined') { + uid = imageUids[this.getSecondaryOffset(index)]; + } + return uid; }; /** * Get the geometry of the image. * - * @returns {object} The size of the image. + * @returns {dwv.image.Geometry} The geometry. */ this.getGeometry = function () { return geometry; @@ -14197,47 +15273,129 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { this.getBuffer = function () { return buffer; }; + /** - * Get the data buffer of the image. + * Can the image values be quantified? * - * @param {number} frame The frame number. - * @todo dangerous... - * @returns {Array} The data buffer of the frame. + * @returns {boolean} True if only one component. + */ + this.canQuantify = function () { + return this.getNumberOfComponents() === 1; + }; + + /** + * Can window and level be applied to the data? + * + * @returns {boolean} True if the data is monochrome. + */ + this.canWindowLevel = function () { + return this.getPhotometricInterpretation() + .match(/MONOCHROME/) !== null; + }; + + /** + * Can the data be scrolled? + * + * @param {dwv.math.Matrix33} viewOrientation The view orientation. + * @returns {boolean} True if the data has a third dimension greater than one + * after applying the view orientation. + */ + this.canScroll = function (viewOrientation) { + var size = this.getGeometry().getSize(); + // also check the numberOfFiles in case we are in the middle of a load + var nFiles = 1; + if (typeof meta.numberOfFiles !== 'undefined') { + nFiles = meta.numberOfFiles; + } + return size.canScroll(viewOrientation) || nFiles !== 1; + }; + + /** + * Get the secondary offset max. + * + * @returns {number} The maximum offset. + */ + function getSecondaryOffsetMax() { + return geometry.getSize().getTotalSize(2); + } + + /** + * Get the secondary offset: an offset that takes into account + * the slice and above dimension numbers. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The offset. */ - this.getFrame = function (frame) { - return buffer[frame]; + this.getSecondaryOffset = function (index) { + return geometry.getSize().indexToOffset(index, 2); }; /** * Get the rescale slope and intercept. * - * @param {number} k The slice index. + * @param {dwv.math.Index} index The index (only needed for non constant rsi). * @returns {object} The rescale slope and intercept. */ - this.getRescaleSlopeAndIntercept = function (k) { - return rsis[k]; + this.getRescaleSlopeAndIntercept = function (index) { + var res = rsi; + if (!this.isConstantRSI()) { + if (typeof index === 'undefined') { + throw new Error('Cannot get non constant RSI with empty slice index.'); + } + var offset = this.getSecondaryOffset(index); + if (typeof rsis[offset] !== 'undefined') { + res = rsis[offset]; + } else { + dwv.logger.warn('undefined non constant rsi at ' + offset); + } + } + return res; }; + /** - * Set the rescale slope and intercept. + * Get the rsi at a specified (secondary) offset. * - * @param {Array} inRsi The input rescale slope and intercept. - * @param {number} k The slice index (optional). + * @param {number} offset The desired (secondary) offset. + * @returns {object} The coresponding rsi. */ - this.setRescaleSlopeAndIntercept = function (inRsi, k) { - if (typeof k === 'undefined') { - k = 0; - } - rsis[k] = inRsi; + function getRescaleSlopeAndInterceptAtOffset(offset) { + return rsis[offset]; + } - // update RSI flags - isIdentityRSI = true; - isConstantRSI = true; - for (var s = 0, lens = rsis.length; s < lens; ++s) { - if (!rsis[s].isID()) { - isIdentityRSI = false; + /** + * Set the rescale slope and intercept. + * + * @param {object} inRsi The input rescale slope and intercept. + * @param {number} offset The rsi offset (only needed for non constant rsi). + */ + this.setRescaleSlopeAndIntercept = function (inRsi, offset) { + // update identity flag + isIdentityRSI = isIdentityRSI && inRsi.isID(); + // update constant flag + if (!isConstantRSI) { + if (typeof index === 'undefined') { + throw new Error( + 'Cannot store non constant RSI with empty slice index.'); } - if (s > 0 && !rsis[s].equals(rsis[s - 1])) { - isConstantRSI = false; + rsis.splice(offset, 0, inRsi); + } else { + if (!rsi.equals(inRsi)) { + if (typeof index === 'undefined') { + // no slice index, replace existing + rsi = inRsi; + } else { + // first non constant rsi + isConstantRSI = false; + // switch to non constant mode + rsis = []; + // initialise RSIs + for (var i = 0, leni = getSecondaryOffsetMax(); i < leni; ++i) { + rsis.push(i); + } + // store + rsi = null; + rsis.splice(offset, 0, inRsi); + } } } }; @@ -14319,11 +15477,10 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { * Get value at offset. Warning: No size check... * * @param {number} offset The desired offset. - * @param {number} frame The desired frame. * @returns {number} The value at offset. */ - this.getValueAtOffset = function (offset, frame) { - return buffer[frame][offset]; + this.getValueAtOffset = function (offset) { + return buffer[offset]; }; /** @@ -14333,16 +15490,17 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { */ this.clone = function () { // clone the image buffer - var clonedBuffer = []; - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - clonedBuffer[f] = buffer[f].slice(0); - } + var clonedBuffer = buffer.slice(0); // create the image copy - var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer); - // copy the RSIs - var nslices = this.getGeometry().getSize().getNumberOfSlices(); - for (var k = 0; k < nslices; ++k) { - copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept(k), k); + var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer, imageUids); + // copy the RSI(s) + if (this.isConstantRSI()) { + copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept()); + } else { + for (var i = 0; i < getSecondaryOffsetMax(); ++i) { + copy.setRescaleSlopeAndIntercept( + getRescaleSlopeAndInterceptAtOffset(i), i); + } } // copy extras copy.setPhotometricInterpretation(this.getPhotometricInterpretation()); @@ -14352,27 +15510,51 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { return copy; }; + /** + * Re-allocate buffer memory to an input size. + * + * @param {number} size The new size. + */ + function realloc(size) { + // save buffer + var tmpBuffer = buffer; + // create new + buffer = dwv.dicom.getTypedArray( + buffer.BYTES_PER_ELEMENT * 8, + meta.IsSigned ? 1 : 0, + size); + if (buffer === null) { + throw new Error('Cannot reallocate data for image.'); + } + // put old in new + buffer.set(tmpBuffer); + // clean + tmpBuffer = null; + } + /** * Append a slice to the image. * * @param {Image} rhs The slice to append. - * @param {number} frame The frame where to append. - * @returns {number} The number of the inserted slice. + * @param {number} timeId An optional time ID. */ - this.appendSlice = function (rhs, frame) { + this.appendSlice = function (rhs, timeId) { + if (typeof timeId === 'undefined') { + timeId = 0; + } // check input if (rhs === null) { throw new Error('Cannot append null slice'); } var rhsSize = rhs.getGeometry().getSize(); var size = geometry.getSize(); - if (rhsSize.getNumberOfSlices() !== 1) { + if (rhsSize.get(2) !== 1) { throw new Error('Cannot append more than one slice'); } - if (size.getNumberOfColumns() !== rhsSize.getNumberOfColumns()) { + if (size.get(0) !== rhsSize.get(0)) { throw new Error('Cannot append a slice with different number of columns'); } - if (size.getNumberOfRows() !== rhsSize.getNumberOfRows()) { + if (size.get(1) !== rhsSize.get(1)) { throw new Error('Cannot append a slice with different number of rows'); } if (!geometry.getOrientation().equals( @@ -14393,47 +15575,73 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { } } - var f = (typeof frame === 'undefined') ? 0 : frame; - // calculate slice size - var mul = 1; - if (photometricInterpretation === 'RGB' || - photometricInterpretation === 'YBR_FULL') { - mul = 3; + var sliceSize = numberOfComponents * size.getDimSize(2); + + // create full buffer if not done yet + if (typeof meta.numberOfFiles === 'undefined') { + throw new Error('Missing number of files for buffer manipulation.'); + } + var fullBufferSize = sliceSize * meta.numberOfFiles; + if (size.length() === 4) { + fullBufferSize *= size.get(3); + } + if (buffer.length !== fullBufferSize) { + realloc(fullBufferSize); + } + + var newSliceIndex = geometry.getSliceIndex(rhs.getGeometry().getOrigin()); + var values = new Array(geometry.getSize().length()); + values.fill(0); + values[2] = newSliceIndex; + if (size.length() === 4) { + values[3] = timeId; + } + var index = new dwv.math.Index(values); + var primaryOffset = size.indexToOffset(index); + var secondaryOffset = this.getSecondaryOffset(index); + + // first frame special slice by slice append + if (timeId === 0) { + // store slice + var oldNumberOfSlices = size.get(2); + // move content if needed + var start = primaryOffset; + var end; + if (newSliceIndex === 0) { + // insert slice before current data + end = start + oldNumberOfSlices * sliceSize; + buffer.set( + buffer.subarray(start, end), + primaryOffset + sliceSize + ); + } else if (newSliceIndex < oldNumberOfSlices) { + // insert slice in between current data + end = start + (oldNumberOfSlices - newSliceIndex) * sliceSize; + buffer.set( + buffer.subarray(start, end), + primaryOffset + sliceSize + ); + } } - var sliceSize = mul * size.getSliceSize(); - // create the new buffer - var newBuffer = dwv.dicom.getTypedArray( - buffer[f].BYTES_PER_ELEMENT * 8, - meta.IsSigned ? 1 : 0, - sliceSize * (size.getNumberOfSlices() + 1)); - - // append slice at new position - var newSliceNb = geometry.getSliceIndex(rhs.getGeometry().getOrigin()); - if (newSliceNb === 0) { - newBuffer.set(rhs.getFrame(f)); - newBuffer.set(buffer[f], sliceSize); - } else if (newSliceNb === size.getNumberOfSlices()) { - newBuffer.set(buffer[f]); - newBuffer.set(rhs.getFrame(f), size.getNumberOfSlices() * sliceSize); - } else { - var offset = newSliceNb * sliceSize; - newBuffer.set(buffer[f].subarray(0, offset - 1)); - newBuffer.set(rhs.getFrame(f), offset); - newBuffer.set(buffer[f].subarray(offset), offset + sliceSize); - } + // add new slice content + buffer.set(rhs.getBuffer(), primaryOffset); // update geometry - geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceNb); + if (timeId === 0) { + geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceIndex); + } // update rsi - rsis.splice(newSliceNb, 0, rhs.getRescaleSlopeAndIntercept(0)); + // (rhs should just have one rsi) + this.setRescaleSlopeAndIntercept( + rhs.getRescaleSlopeAndIntercept(), secondaryOffset); - // copy to class variables - buffer[f] = newBuffer; + // current number of images + var numberOfImages = imageUids.length; // insert sop instance UIDs - imageUids.splice(newSliceNb, 0, rhs.getImageUids()[0]); + imageUids.splice(secondaryOffset, 0, rhs.getImageUid()); // update window presets if (typeof meta.windowPresets !== 'undefined') { @@ -14443,33 +15651,70 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { var pkey = null; for (var i = 0; i < keys.length; ++i) { pkey = keys[i]; - if (typeof windowPresets[pkey] !== 'undefined') { - if (typeof windowPresets[pkey].perslice !== 'undefined' && - windowPresets[pkey].perslice === true) { - // use first new preset wl... + var rhsPreset = rhsPresets[pkey]; + var windowPreset = windowPresets[pkey]; + if (typeof windowPreset !== 'undefined') { + // if not set or false, check perslice + if (typeof windowPreset.perslice === 'undefined' || + windowPreset.perslice === false) { + // if different preset.wl, mark it as perslice + if (!windowPreset.wl[0].equals(rhsPreset.wl[0])) { + windowPreset.perslice = true; + // fill wl array with copy of wl[0] + // (loop on number of images minus the existing one) + for (var j = 0; j < numberOfImages - 1; ++j) { + windowPreset.wl.push(windowPreset.wl[0]); + } + } + } + // store (first) rhs preset.wl if needed + if (typeof windowPreset.perslice !== 'undefined' && + windowPreset.perslice === true) { windowPresets[pkey].wl.splice( - newSliceNb, 0, rhsPresets[pkey].wl[0]); - } else { - windowPresets[pkey] = rhsPresets[pkey]; + secondaryOffset, 0, rhsPreset.wl[0]); } } else { - // update + // if not defined (it should be), store all windowPresets[pkey] = rhsPresets[pkey]; } } } - - // return the appended slice number - return newSliceNb; }; /** * Append a frame buffer to the image. * * @param {object} frameBuffer The frame buffer to append. + * @param {number} frameIndex The frame index. + */ + this.appendFrameBuffer = function (frameBuffer, frameIndex) { + // create full buffer if not done yet + var size = geometry.getSize(); + var frameSize = numberOfComponents * size.getDimSize(2); + if (typeof meta.numberOfFiles === 'undefined') { + throw new Error('Missing number of files for frame buffer manipulation.'); + } + var fullBufferSize = frameSize * meta.numberOfFiles; + if (buffer.length !== fullBufferSize) { + realloc(fullBufferSize); + } + // append + if (frameIndex >= meta.numberOfFiles) { + throw new Error( + 'Cannot append a frame at an index above the number of frames'); + } + buffer.set(frameBuffer, frameSize * frameIndex); + // update geometry + this.appendFrame(); + }; + + /** + * Append a frame to the image. */ - this.appendFrameBuffer = function (frameBuffer) { - buffer.push(frameBuffer); + this.appendFrame = function () { + geometry.appendFrame(); + fireEvent({type: 'appendframe'}); + // memory will be updated at the first appendSlice or appendFrameBuffer }; /** @@ -14510,6 +15755,38 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { } return histogram; }; + + /** + * Add an event listener to this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + + /** + * Remove an event listener from this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + + /** + * Fire an event: call all associated listeners with the input event object. + * + * @param {object} event The event to fire. + * @private + */ + function fireEvent(event) { + listenerHandler.fireEvent(event); + } }; /** @@ -14524,12 +15801,25 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { */ dwv.image.Image.prototype.getValue = function (i, j, k, f) { var frame = (f || 0); - var index = new dwv.math.Index3D(i, j, k); - return this.getValueAtOffset(this.getGeometry().indexToOffset(index), frame); + var index = new dwv.math.Index([i, j, k, frame]); + return this.getValueAtOffset( + this.getGeometry().getSize().indexToOffset(index)); +}; + +/** + * Get the value of the image at a specific index. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getValueAtIndex = function (index) { + return this.getValueAtOffset( + this.getGeometry().getSize().indexToOffset(index)); }; /** - * Get the rescaled value of the image at a specific coordinate. + * Get the rescaled value of the image at a specific position. * * @param {number} i The X index. * @param {number} j The Y index. @@ -14539,28 +15829,51 @@ dwv.image.Image.prototype.getValue = function (i, j, k, f) { * Warning: No size check... */ dwv.image.Image.prototype.getRescaledValue = function (i, j, k, f) { - var frame = (f || 0); - var val = this.getValue(i, j, k, frame); + if (typeof f === 'undefined') { + f = 0; + } + var val = this.getValue(i, j, k, f); if (!this.isIdentityRSI()) { - val = this.getRescaleSlopeAndIntercept(k).apply(val); + if (this.isConstantRSI()) { + val = this.getRescaleSlopeAndIntercept().apply(val); + } else { + var values = [i, j, k, f]; + var index = new dwv.math.Index(values); + val = this.getRescaleSlopeAndIntercept(index).apply(val); + } } return val; }; +/** + * Get the rescaled value of the image at a specific index. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The rescaled value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getRescaledValueAtIndex = function (index) { + return this.getRescaledValueAtOffset( + this.getGeometry().getSize().indexToOffset(index) + ); +}; + /** * Get the rescaled value of the image at a specific offset. * * @param {number} offset The desired offset. - * @param {number} k The Z index. - * @param {number} f The frame number. * @returns {number} The rescaled value at the desired offset. * Warning: No size check... */ -dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset, k, f) { - var frame = (f || 0); - var val = this.getValueAtOffset(offset, frame); +dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset) { + var val = this.getValueAtOffset(offset); if (!this.isIdentityRSI()) { - val = this.getRescaleSlopeAndIntercept(k).apply(val); + if (this.isConstantRSI()) { + val = this.getRescaleSlopeAndIntercept().apply(val); + } else { + var index = this.getGeometry().getSize().offsetToIndex(offset); + val = this.getRescaleSlopeAndIntercept(index).apply(val); + } } return val; }; @@ -14572,20 +15885,22 @@ dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset, k, f) { * @returns {object} The range {min, max}. */ dwv.image.Image.prototype.calculateDataRange = function () { - var size = this.getGeometry().getSize().getTotalSize(); - var nFrames = 1; //this.getNumberOfFrames(); - var min = this.getValueAtOffset(0, 0); + var min = this.getValueAtOffset(0); var max = min; var value = 0; - for (var f = 0; f < nFrames; ++f) { - for (var i = 0; i < size; ++i) { - value = this.getValueAtOffset(i, f); - if (value > max) { - max = value; - } - if (value < min) { - min = value; - } + var size = this.getGeometry().getSize(); + var leni = size.getTotalSize(); + // max to 3D + if (size.length() >= 3) { + leni = size.getDimSize(3); + } + for (var i = 0; i < leni; ++i) { + value = this.getValueAtOffset(i); + if (value > max) { + max = value; + } + if (value < min) { + min = value; } } // return @@ -14603,31 +15918,29 @@ dwv.image.Image.prototype.calculateRescaledDataRange = function () { return this.getDataRange(); } else if (this.isConstantRSI()) { var range = this.getDataRange(); - var resmin = this.getRescaleSlopeAndIntercept(0).apply(range.min); - var resmax = this.getRescaleSlopeAndIntercept(0).apply(range.max); + var resmin = this.getRescaleSlopeAndIntercept().apply(range.min); + var resmax = this.getRescaleSlopeAndIntercept().apply(range.max); return { min: ((resmin < resmax) ? resmin : resmax), max: ((resmin > resmax) ? resmin : resmax) }; } else { - var size = this.getGeometry().getSize(); - var nFrames = 1; //this.getNumberOfFrames(); - var rmin = this.getRescaledValue(0, 0, 0); + var rmin = this.getRescaledValueAtOffset(0); var rmax = rmin; var rvalue = 0; - for (var f = 0, nframes = nFrames; f < nframes; ++f) { - for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) { - for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) { - for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) { - rvalue = this.getRescaledValue(i, j, k, f); - if (rvalue > rmax) { - rmax = rvalue; - } - if (rvalue < rmin) { - rmin = rvalue; - } - } - } + var size = this.getGeometry().getSize(); + var leni = size.getTotalSize(); + // max to 3D + if (size.length() === 3) { + leni = size.getDimSize(3); + } + for (var i = 0; i < leni; ++i) { + rvalue = this.getRescaledValueAtOffset(i); + if (rvalue > rmax) { + rmax = rvalue; + } + if (rvalue < rmin) { + rmin = rvalue; } } // return @@ -14643,34 +15956,28 @@ dwv.image.Image.prototype.calculateRescaledDataRange = function () { dwv.image.Image.prototype.calculateHistogram = function () { var size = this.getGeometry().getSize(); var histo = []; - var min = this.getValue(0, 0, 0); + var min = this.getValueAtOffset(0); var max = min; var value = 0; - var rmin = this.getRescaledValue(0, 0, 0); + var rmin = this.getRescaledValueAtOffset(0); var rmax = rmin; - var rvalue = 0; - for (var f = 0, nframes = this.getNumberOfFrames(); f < nframes; ++f) { - for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) { - for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) { - for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) { - value = this.getValue(i, j, k, f); - if (value > max) { - max = value; - } - if (value < min) { - min = value; - } - rvalue = this.getRescaleSlopeAndIntercept(k).apply(value); - if (rvalue > rmax) { - rmax = rvalue; - } - if (rvalue < rmin) { - rmin = rvalue; - } - histo[rvalue] = (histo[rvalue] || 0) + 1; - } - } + var rvalue = 0; + for (var i = 0, leni = size.getTotalSize(); i < leni; ++i) { + value = this.getValueAtOffset(i); + if (value > max) { + max = value; + } + if (value < min) { + min = value; + } + rvalue = this.getRescaledValueAtOffset(i); + if (rvalue > rmax) { + rmax = rvalue; + } + if (rvalue < rmin) { + rmin = rvalue; } + histo[rvalue] = (histo[rvalue] || 0) + 1; } // set data range var dataRange = {min: min, max: max}; @@ -14691,9 +15998,10 @@ dwv.image.Image.prototype.calculateHistogram = function () { /** * Convolute the image with a given 2D kernel. * + * Note: Uses raw buffer values. + * * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix. * @returns {Image} The convoluted image. - * Note: Uses the raw buffer values. */ dwv.image.Image.prototype.convolute2D = function (weights) { if (weights.length !== 9) { @@ -14706,22 +16014,38 @@ dwv.image.Image.prototype.convolute2D = function (weights) { var newBuffer = newImage.getBuffer(); var imgSize = this.getGeometry().getSize(); - var ncols = imgSize.getNumberOfColumns(); - var nrows = imgSize.getNumberOfRows(); - var nslices = imgSize.getNumberOfSlices(); - var nframes = this.getNumberOfFrames(); + var dimOffset = imgSize.getDimSize(2) * this.getNumberOfComponents(); + for (var k = 0; k < imgSize.get(2); ++k) { + this.convoluteBuffer(weights, newBuffer, k * dimOffset); + } + + return newImage; +}; + +/** + * Convolute an image buffer with a given 2D kernel. + * + * Note: Uses raw buffer values. + * + * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix. + * @param {Array} buffer The buffer to convolute. + * @param {number} startOffset The index to start at. + */ +dwv.image.Image.prototype.convoluteBuffer = function ( + weights, buffer, startOffset) { + var imgSize = this.getGeometry().getSize(); + var ncols = imgSize.get(0); + var nrows = imgSize.get(1); var ncomp = this.getNumberOfComponents(); - // adapt to number of component and planar configuration + // number of component and planar configuration vars var factor = 1; var componentOffset = 1; - var frameOffset = imgSize.getTotalSize(); if (ncomp === 3) { - frameOffset *= 3; if (this.getPlanarConfiguration() === 0) { factor = 3; } else { - componentOffset = imgSize.getTotalSize(); + componentOffset = imgSize.getDimSize(2); } } @@ -14791,54 +16115,46 @@ dwv.image.Image.prototype.convolute2D = function (weights) { /*jshint indent:4 */ // loop vars - var pixelOffset = 0; + var pixelOffset = startOffset; var newValue = 0; var wOffFinal = []; - // go through the destination image pixels - for (var f = 0; f < nframes; f++) { - pixelOffset = f * frameOffset; - for (var c = 0; c < ncomp; c++) { - // special component offset - pixelOffset += c * componentOffset; - for (var k = 0; k < nslices; k++) { - for (var j = 0; j < nrows; j++) { - for (var i = 0; i < ncols; i++) { - wOffFinal = wOff; - // special border cases - if (i === 0 && j === 0) { - wOffFinal = wOff00; - } else if (i === 0 && j === (nrows - 1)) { - wOffFinal = wOff0n; - } else if (i === (ncols - 1) && j === 0) { - wOffFinal = wOffn0; - } else if (i === (ncols - 1) && j === (nrows - 1)) { - wOffFinal = wOffnn; - } else if (i === 0 && j !== (nrows - 1) && j !== 0) { - wOffFinal = wOff0x; - } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) { - wOffFinal = wOffnx; - } else if (i !== 0 && i !== (ncols - 1) && j === 0) { - wOffFinal = wOffx0; - } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) { - wOffFinal = wOffxn; - } - - // calculate the weighed sum of the source image pixels that - // fall under the convolution matrix - newValue = 0; - for (var wi = 0; wi < 9; ++wi) { - newValue += this.getValueAtOffset( - pixelOffset + wOffFinal[wi], f) * weights[wi]; - } - newBuffer[f][pixelOffset] = newValue; - // increment pixel offset - pixelOffset += factor; - } + for (var c = 0; c < ncomp; ++c) { + // component offset + pixelOffset += c * componentOffset; + for (var j = 0; j < nrows; ++j) { + for (var i = 0; i < ncols; ++i) { + wOffFinal = wOff; + // special border cases + if (i === 0 && j === 0) { + wOffFinal = wOff00; + } else if (i === 0 && j === (nrows - 1)) { + wOffFinal = wOff0n; + } else if (i === (ncols - 1) && j === 0) { + wOffFinal = wOffn0; + } else if (i === (ncols - 1) && j === (nrows - 1)) { + wOffFinal = wOffnn; + } else if (i === 0 && j !== (nrows - 1) && j !== 0) { + wOffFinal = wOff0x; + } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) { + wOffFinal = wOffnx; + } else if (i !== 0 && i !== (ncols - 1) && j === 0) { + wOffFinal = wOffx0; + } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) { + wOffFinal = wOffxn; + } + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + newValue = 0; + for (var wi = 0; wi < 9; ++wi) { + newValue += this.getValueAtOffset( + pixelOffset + wOffFinal[wi]) * weights[wi]; } + buffer[pixelOffset] = newValue; + // increment pixel offset + pixelOffset += factor; } } } - return newImage; }; /** @@ -14852,10 +16168,8 @@ dwv.image.Image.prototype.convolute2D = function (weights) { dwv.image.Image.prototype.transform = function (operator) { var newImage = this.clone(); var newBuffer = newImage.getBuffer(); - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) { - newBuffer[f][i] = operator(newImage.getValueAtOffset(i, f)); - } + for (var i = 0, leni = newBuffer.length; i < leni; ++i) { + newBuffer[i] = operator(newImage.getValueAtOffset(i)); } return newImage; }; @@ -14872,14 +16186,12 @@ dwv.image.Image.prototype.transform = function (operator) { dwv.image.Image.prototype.compose = function (rhs, operator) { var newImage = this.clone(); var newBuffer = newImage.getBuffer(); - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) { - // using the operator on the local buffer, i.e. the - // latest (not original) data - newBuffer[f][i] = Math.floor( - operator(this.getValueAtOffset(i, f), rhs.getValueAtOffset(i, f)) - ); - } + for (var i = 0, leni = newBuffer.length; i < leni; ++i) { + // using the operator on the local buffer, i.e. the + // latest (not original) data + newBuffer[i] = Math.floor( + operator(this.getValueAtOffset(i), rhs.getValueAtOffset(i)) + ); } return newImage; }; @@ -14895,15 +16207,41 @@ dwv.image = dwv.image || {}; */ dwv.image.ImageFactory = function () {}; +/** + * {@link dwv.image.Image} factory. Defaults to local one. + * + * @see dwv.image.ImageFactory + */ +dwv.ImageFactory = dwv.image.ImageFactory; + +/** + * Check dicom elements. Throws an error if not suitable. + * + * @param {object} dicomElements The DICOM tags. + */ +dwv.image.ImageFactory.prototype.checkElements = function (dicomElements) { + // columns + var columns = dicomElements.getFromKey('x00280011'); + if (!columns) { + throw new Error('Missing or empty DICOM image number of columns'); + } + // rows + var rows = dicomElements.getFromKey('x00280010'); + if (!rows) { + throw new Error('Missing or empty DICOM image number of rows'); + } +}; + /** * Get an {@link dwv.image.Image} object from the read DICOM file. * * @param {object} dicomElements The DICOM tags. * @param {Array} pixelBuffer The pixel buffer. + * @param {number} numberOfFiles The input number of files. * @returns {dwv.image.Image} A new Image. */ dwv.image.ImageFactory.prototype.create = function ( - dicomElements, pixelBuffer) { + dicomElements, pixelBuffer, numberOfFiles) { // columns var columns = dicomElements.getFromKey('x00280011'); if (!columns) { @@ -14914,27 +16252,20 @@ dwv.image.ImageFactory.prototype.create = function ( if (!rows) { throw new Error('Missing or empty DICOM image number of rows'); } - // image size - var size = new dwv.image.Size(columns, rows); - - // spacing - var rowSpacing = null; - var columnSpacing = null; - // PixelSpacing - var pixelSpacing = dicomElements.getFromKey('x00280030'); - // ImagerPixelSpacing - var imagerPixelSpacing = dicomElements.getFromKey('x00181164'); - if (pixelSpacing && pixelSpacing[0] && pixelSpacing[1]) { - rowSpacing = parseFloat(pixelSpacing[0]); - columnSpacing = parseFloat(pixelSpacing[1]); - } else if (imagerPixelSpacing && - imagerPixelSpacing[0] && - imagerPixelSpacing[1]) { - rowSpacing = parseFloat(imagerPixelSpacing[0]); - columnSpacing = parseFloat(imagerPixelSpacing[1]); + + var sizeValues = [columns, rows, 1]; + + // frames + var frames = dicomElements.getFromKey('x00280008'); + if (frames) { + sizeValues.push(frames); } + + // image size + var size = new dwv.image.Size(sizeValues); + // image spacing - var spacing = new dwv.image.Spacing(columnSpacing, rowSpacing); + var spacing = dicomElements.getPixelSpacing(); // TransferSyntaxUID var transferSyntaxUID = dicomElements.getFromKey('x00020010'); @@ -14945,22 +16276,16 @@ dwv.image.ImageFactory.prototype.create = function ( // ImagePositionPatient var imagePositionPatient = dicomElements.getFromKey('x00200032'); - // InstanceNumber - var instanceNumber = dicomElements.getFromKey('x00200013'); - // slice position var slicePosition = new Array(0, 0, 0); if (imagePositionPatient) { slicePosition = [parseFloat(imagePositionPatient[0]), parseFloat(imagePositionPatient[1]), parseFloat(imagePositionPatient[2])]; - } else if (instanceNumber) { - // use instanceNumber as slice index if no imagePositionPatient was provided - dwv.logger.warn('Using instanceNumber as imagePositionPatient.'); - slicePosition[2] = parseInt(instanceNumber, 10); } - // slice orientation + // slice orientation (cosines are matrices' columns) + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 var imageOrientationPatient = dicomElements.getFromKey('x00200037'); var orientationMatrix; if (imageOrientationPatient) { @@ -14973,10 +16298,13 @@ dwv.image.ImageFactory.prototype.create = function ( parseFloat(imageOrientationPatient[4]), parseFloat(imageOrientationPatient[5])); var normal = rowCosines.crossProduct(colCosines); - orientationMatrix = new dwv.math.Matrix33( - rowCosines.getX(), rowCosines.getY(), rowCosines.getZ(), - colCosines.getX(), colCosines.getY(), colCosines.getZ(), - normal.getX(), normal.getY(), normal.getZ()); + /* eslint-disable array-element-newline */ + orientationMatrix = new dwv.math.Matrix33([ + rowCosines.getX(), colCosines.getX(), normal.getX(), + rowCosines.getY(), colCosines.getY(), normal.getY(), + rowCosines.getZ(), colCosines.getZ(), normal.getZ() + ]); + /* eslint-enable array-element-newline */ } // geometry @@ -14989,10 +16317,27 @@ dwv.image.ImageFactory.prototype.create = function ( var sopInstanceUid = dwv.dicom.cleanString( dicomElements.getFromKey('x00080018')); + // Sample per pixels + var samplesPerPixel = dicomElements.getFromKey('x00280002'); + if (!samplesPerPixel) { + samplesPerPixel = 1; + } + + // check buffer size + var bufferSize = size.getTotalSize() * samplesPerPixel; + if (bufferSize !== pixelBuffer.length) { + dwv.logger.warn('Badly sized pixel buffer: ' + + pixelBuffer.length + ' != ' + bufferSize); + if (bufferSize < pixelBuffer.length) { + pixelBuffer = pixelBuffer.slice(0, size.getTotalSize()); + } else { + throw new Error('Underestimated buffer size, can\'t fix it...'); + } + } + // image - var image = new dwv.image.Image( - geometry, pixelBuffer, pixelBuffer.length, [sopInstanceUid]); - // PhotometricInterpretation + var image = new dwv.image.Image(geometry, pixelBuffer, [sopInstanceUid]); + // PhotometricInterpretation var photometricInterpretation = dicomElements.getFromKey('x00280004'); if (photometricInterpretation) { var photo = dwv.dicom.cleanString(photometricInterpretation).toUpperCase(); @@ -15002,7 +16347,6 @@ dwv.image.ImageFactory.prototype.create = function ( photo = 'RGB'; } // check samples per pixels - var samplesPerPixel = parseInt(dicomElements.getFromKey('x00280002'), 10); if (photo === 'RGB' && samplesPerPixel === 1) { photo = 'PALETTE COLOR'; } @@ -15032,6 +16376,8 @@ dwv.image.ImageFactory.prototype.create = function ( // meta information var meta = {}; + // data length + meta.numberOfFiles = numberOfFiles; // Modality var modality = dicomElements.getFromKey('x00080060'); if (modality) { @@ -15058,6 +16404,12 @@ dwv.image.ImageFactory.prototype.create = function ( if (pixelRepresentation) { meta.IsSigned = (pixelRepresentation === 1); } + // PatientPosition + var patientPosition = dicomElements.getFromKey('x00185100'); + meta.PatientPosition = false; + if (patientPosition) { + meta.PatientPosition = patientPosition; + } // window level presets var windowPresets = {}; @@ -15079,8 +16431,7 @@ dwv.image.ImageFactory.prototype.create = function ( } windowPresets[name] = { wl: [new dwv.image.WindowLevel(center, width)], - name: name, - perslice: true + name: name }; } if (width === 0) { @@ -15178,7 +16529,7 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; /** - * Get an iterator for a given range for a one component data. + * Get an simple iterator for a given range for a one component data. * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols * @param {Function} dataAccessor Function to access data. @@ -15187,7 +16538,7 @@ dwv.image = dwv.image || {}; * @param {number} increment The increment between indicies (default=1). * @returns {object} An iterator folowing the iterator and iterable protocol. */ -dwv.image.range = function (dataAccessor, start, end, increment) { +dwv.image.simpleRange = function (dataAccessor, start, end, increment) { if (typeof increment === 'undefined') { increment = 1; } @@ -15211,6 +16562,82 @@ dwv.image.range = function (dataAccessor, start, end, increment) { }; }; +/** + * Get an iterator for a given range for a one component data. + * + * Using 'maxIter' and not an 'end' index since it fails in some edge cases + * (for ex coronal2, ie zxy) + * + * @param {Function} dataAccessor Function to access data. + * @param {number} start Zero-based index at which to start the iteration. + * @param {number} maxIter The maximum number of iterations. + * @param {number} increment Increment between indicies. + * @param {number} blockMaxIter Number of applied increment after which + * blockIncrement is applied. + * @param {number} blockIncrement Increment after blockMaxIter is reached, + * the value is from block start to the next block start. + * @param {boolean} reverse1 If true, loop from end to start. + * WARN: don't forget to set the value of start as the last index! + * @param {boolean} reverse2 If true, loop from block end to block start. + * @returns {object} An iterator folowing the iterator and iterable protocol. + */ +dwv.image.range = function (dataAccessor, start, maxIter, increment, + blockMaxIter, blockIncrement, reverse1, reverse2) { + if (typeof reverse1 === 'undefined') { + reverse1 = false; + } + if (typeof reverse2 === 'undefined') { + reverse2 = false; + } + + // first index of the iteration + var nextIndex = start; + // adapt first index and increments to reverse values + if (reverse1) { + blockIncrement *= -1; + if (reverse2) { + // start at end of line + nextIndex -= (blockMaxIter - 1) * increment; + } else { + increment *= -1; + } + } else { + if (reverse2) { + // start at end of line + nextIndex += (blockMaxIter - 1) * increment; + increment *= -1; + } + } + var finalBlockIncrement = blockIncrement - blockMaxIter * increment; + + // counters + var mainCount = 0; + var blockCount = 0; + // result + return { + next: function () { + if (mainCount < maxIter) { + var result = { + value: dataAccessor(nextIndex), + done: false + }; + nextIndex += increment; + ++mainCount; + ++blockCount; + if (blockCount === blockMaxIter) { + blockCount = 0; + nextIndex += finalBlockIncrement; + } + return result; + } + return { + done: true, + index: nextIndex + }; + } + }; +}; + /** * Get an iterator for a given range with bounds (for a one component data). * @@ -15373,22 +16800,102 @@ dwv.image.getIteratorValues = function (iterator) { /** * Get a slice index iterator. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. + * @param {boolean} isRescaled Flag for rescaled values (default false). + * @param {dwv.math.Matrix33} viewOrientation The view orientation. * @returns {object} The slice iterator. */ -dwv.image.getSliceIterator = function (image, slice, frame) { - var sliceSize = image.getGeometry().getSize().getSliceSize(); - var start = slice * sliceSize; - - var dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); +dwv.image.getSliceIterator = function ( + image, position, isRescaled, viewOrientation) { + var size = image.getGeometry().getSize(); + // zero-ify non direction index + var dirMax2Index = 2; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + dirMax2Index = viewOrientation.getColAbsMax(2).index; + } + var posValues = position.getValues(); + // keep the main direction and any other than 3D + var indexFilter = function (element, index) { + return (index === dirMax2Index || index > 2) ? element : 0; }; + var posStart = new dwv.math.Index(posValues.map(indexFilter)); + var start = size.indexToOffset(posStart); + + // default to non rescaled data + if (typeof isRescaled === 'undefined') { + isRescaled = false; + } + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var ncols = size.get(0); + var nrows = size.get(1); + var nslices = size.get(2); + var sliceSize = size.getDimSize(2); var range = null; if (image.getNumberOfComponents() === 1) { - range = dwv.image.range(dataAccessor, start, start + sliceSize); + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var dirMax0 = viewOrientation.getColAbsMax(0); + var dirMax2 = viewOrientation.getColAbsMax(2); + + // default reverse + var reverse1 = false; + var reverse2 = false; + + var maxIter = null; + if (dirMax2.index === 2) { + // axial + maxIter = ncols * nrows; + if (dirMax0.index === 0) { + // xyz + range = dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, ncols, reverse1, reverse2); + } else { + // yxz + range = dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, 1, reverse1, reverse2); + } + } else if (dirMax2.index === 0) { + // sagittal + maxIter = nslices * nrows; + if (dirMax0.index === 1) { + // yzx + range = dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, sliceSize, reverse1, reverse2); + } else { + // zyx + range = dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, ncols, reverse1, reverse2); + } + } else if (dirMax2.index === 1) { + // coronal + maxIter = nslices * ncols; + if (dirMax0.index === 0) { + // xzy + range = dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, sliceSize, reverse1, reverse2); + } else { + // zxy + range = dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, 1, reverse1, reverse2); + } + } else { + throw new Error('Unknown direction: ' + dirMax2.index); + } + } else { + // default case + range = dwv.image.simpleRange(dataAccessor, start, start + sliceSize); + } } else if (image.getNumberOfComponents() === 3) { // 3 times bigger... start *= 3; @@ -15407,56 +16914,56 @@ dwv.image.getSliceIterator = function (image, slice, frame) { /** * Get a slice index iterator for a rectangular region. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. * @param {boolean} isRescaled Flag for rescaled values (default false). * @param {dwv.math.Point2D} min The minimum position (optional). * @param {dwv.math.Point2D} max The maximum position (optional). * @returns {object} The slice iterator. */ dwv.image.getRegionSliceIterator = function ( - image, slice, frame, isRescaled, min, max) { + image, position, isRescaled, min, max) { if (image.getNumberOfComponents() !== 1) { throw new Error('Unsupported number of components for region iterator: ' + image.getNumberOfComponents()); } + // default to non rescaled data if (typeof isRescaled === 'undefined') { isRescaled = false; } - var geometry = image.getGeometry(); - var size = geometry.getSize(); + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var size = image.getGeometry().getSize(); if (typeof min === 'undefined') { min = new dwv.math.Point2D(0, 0); } if (typeof max === 'undefined') { max = new dwv.math.Point2D( - size.getNumberOfColumns() - 1, - size.getNumberOfRows() + size.get(0) - 1, + size.get(1) ); } // position to pixel for max: extra X is ok, remove extra Y - var minIndex = new dwv.math.Index3D(min.getX(), min.getY(), slice); - var startOffset = geometry.indexToOffset(minIndex); - var maxIndex = new dwv.math.Index3D(max.getX(), max.getY() - 1, slice); - var endOffset = geometry.indexToOffset(maxIndex); + var startOffset = size.indexToOffset(position.getWithNew2D( + min.getX(), min.getY() + )); + var endOffset = size.indexToOffset(position.getWithNew2D( + max.getX(), max.getY() - 1 + )); // minimum 1 column var rangeNumberOfColumns = Math.max(1, max.getX() - min.getX()); - var rowIncrement = size.getNumberOfColumns() - rangeNumberOfColumns; - - // data accessor - var dataAccessor = null; - if (isRescaled) { - dataAccessor = function (offset) { - return image.getRescaledValueAtOffset(offset, slice, frame); - }; - } else { - dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); - }; - } + var rowIncrement = size.get(0) - rangeNumberOfColumns; return dwv.image.rangeRegion( dataAccessor, startOffset, endOffset + 1, @@ -15466,25 +16973,35 @@ dwv.image.getRegionSliceIterator = function ( /** * Get a slice index iterator for a rectangular region. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. * @param {boolean} isRescaled Flag for rescaled values (default false). * @param {Array} regions An array of regions. * @returns {object} The slice iterator. */ dwv.image.getVariableRegionSliceIterator = function ( - image, slice, frame, isRescaled, regions) { + image, position, isRescaled, regions) { if (image.getNumberOfComponents() !== 1) { throw new Error('Unsupported number of components for region iterator: ' + image.getNumberOfComponents()); } + // default to non rescaled data if (typeof isRescaled === 'undefined') { isRescaled = false; } - var geometry = image.getGeometry(); - var size = geometry.getSize(); + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var size = image.getGeometry().getSize(); var offsetRegions = []; var region; @@ -15502,7 +17019,7 @@ dwv.image.getVariableRegionSliceIterator = function ( offsetRegions.push([ region[0][0], width, - size.getNumberOfColumns() - region[1][0] + size.get(0) - region[1][0] ]); } } @@ -15515,22 +17032,12 @@ dwv.image.getVariableRegionSliceIterator = function ( return; } - var minIndex = new dwv.math.Index3D(min[0], min[1], slice); - var startOffset = geometry.indexToOffset(minIndex); - var maxIndex = new dwv.math.Index3D(max[0], max[1], slice); - var endOffset = geometry.indexToOffset(maxIndex); - - // data accessor - var dataAccessor = null; - if (isRescaled) { - dataAccessor = function (offset) { - return image.getRescaledValueAtOffset(offset, slice, frame); - }; - } else { - dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); - }; - } + var startOffset = size.indexToOffset(position.getWithNew2D( + min[0], min[1] + )); + var endOffset = size.indexToOffset(position.getWithNew2D( + max[0], max[1] + )); return dwv.image.rangeRegions( dataAccessor, startOffset, endOffset + 1, @@ -15744,38 +17251,237 @@ dwv.image.lut.pet = { }; /* eslint-enable max-len */ -// hot metal blue -/* eslint-disable max-len */ -dwv.image.lut.hot_metal_blue = { - red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 52, 55, 57, 59, 62, 64, 66, 69, 71, 74, 76, 78, 81, 83, 85, 88, 90, 93, 96, 99, 102, 105, 108, 111, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 166, 169, 172, 175, 178, 181, 184, 187, 190, 194, 198, 201, 205, 209, 213, 217, 221, 224, 228, 232, 236, 240, 244, 247, 251, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], - green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 9, 11, 13, 15, 17, 19, 21, 23, 24, 26, 28, 30, 32, 34, 36, 38, 40, 41, 43, 45, 47, 49, 51, 53, 55, 56, 58, 60, 62, 64, 66, 68, 70, 72, 73, 75, 77, 79, 81, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 105, 107, 109, 111, 113, 115, 117, 119, 120, 122, 124, 126, 128, 130, 132, 134, 136, 137, 139, 141, 143, 145, 147, 149, 151, 152, 154, 156, 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 201, 203, 205, 207, 209, 211, 213, 215, 216, 218, 220, 222, 224, 226, 228, 229, 231, 233, 235, 237, 239, 240, 242, 244, 246, 248, 250, 251, 253, 255], - blue: [0, 2, 4, 6, 8, 10, 12, 14, 16, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 197, 194, 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 153, 150, 144, 138, 132, 126, 121, 115, 109, 103, 97, 91, 85, 79, 74, 68, 62, 56, 50, 47, 44, 41, 38, 35, 32, 29, 26, 24, 21, 18, 15, 12, 9, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59, 62, 65, 68, 71, 74, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 126, 129, 132, 135, 138, 141, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 206, 210, 213, 216, 219, 223, 226, 229, 232, 236, 239, 242, 245, 249, 252, 255] -}; -/* eslint-enable max-len */ +// hot metal blue +/* eslint-disable max-len */ +dwv.image.lut.hot_metal_blue = { + red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 52, 55, 57, 59, 62, 64, 66, 69, 71, 74, 76, 78, 81, 83, 85, 88, 90, 93, 96, 99, 102, 105, 108, 111, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155, 158, 161, 164, 166, 169, 172, 175, 178, 181, 184, 187, 190, 194, 198, 201, 205, 209, 213, 217, 221, 224, 228, 232, 236, 240, 244, 247, 251, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 4, 6, 8, 9, 11, 13, 15, 17, 19, 21, 23, 24, 26, 28, 30, 32, 34, 36, 38, 40, 41, 43, 45, 47, 49, 51, 53, 55, 56, 58, 60, 62, 64, 66, 68, 70, 72, 73, 75, 77, 79, 81, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 105, 107, 109, 111, 113, 115, 117, 119, 120, 122, 124, 126, 128, 130, 132, 134, 136, 137, 139, 141, 143, 145, 147, 149, 151, 152, 154, 156, 158, 160, 162, 164, 166, 168, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 201, 203, 205, 207, 209, 211, 213, 215, 216, 218, 220, 222, 224, 226, 228, 229, 231, 233, 235, 237, 239, 240, 242, 244, 246, 248, 250, 251, 253, 255], + blue: [0, 2, 4, 6, 8, 10, 12, 14, 16, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 117, 119, 121, 123, 125, 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 184, 186, 188, 190, 192, 194, 196, 198, 200, 197, 194, 191, 188, 185, 182, 179, 176, 174, 171, 168, 165, 162, 159, 156, 153, 150, 144, 138, 132, 126, 121, 115, 109, 103, 97, 91, 85, 79, 74, 68, 62, 56, 50, 47, 44, 41, 38, 35, 32, 29, 26, 24, 21, 18, 15, 12, 9, 6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 6, 9, 12, 15, 18, 21, 24, 26, 29, 32, 35, 38, 41, 44, 47, 50, 53, 56, 59, 62, 65, 68, 71, 74, 76, 79, 82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 126, 129, 132, 135, 138, 141, 144, 147, 150, 153, 156, 159, 162, 165, 168, 171, 174, 176, 179, 182, 185, 188, 191, 194, 197, 200, 203, 206, 210, 213, 216, 219, 223, 226, 229, 232, 236, 239, 242, 245, 249, 252, 255] +}; +/* eslint-enable max-len */ + +// pet 20 step +/* eslint-disable max-len */ +dwv.image.lut.pet_20step = { + red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], + blue: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] +}; +/* eslint-enable max-len */ + +// test +dwv.image.lut.test = { + red: dwv.image.lut.buildLut(dwv.image.lut.id), + green: dwv.image.lut.buildLut(dwv.image.lut.zero), + blue: dwv.image.lut.buildLut(dwv.image.lut.zero) +}; + +//red +/*dwv.image.lut.red = { + "red": dwv.image.lut.buildLut(dwv.image.lut.max), + "green": dwv.image.lut.buildLut(dwv.image.lut.id), + "blue": dwv.image.lut.buildLut(dwv.image.lut.id) +};*/ + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Plane geometry helper. + * + * @class + * @param {dwv.image.Spacing} spacing The spacing. + * @param {dwv.math.Matrix} orientation The orientation. + */ +dwv.image.PlaneHelper = function (spacing, orientation) { + + /** + * Get a 3D offset from a plane one. + * + * @param {object} offset2D The plane offset as {x,y}. + * @returns {dwv.math.Vector3D} The 3D world offset. + */ + this.getOffset3DFromPlaneOffset = function (offset2D) { + // make 3D + var planeOffset = new dwv.math.Vector3D( + offset2D.x, offset2D.y, 0); + // de-orient + var pixelOffset = this.getDeOrientedVector3D(planeOffset); + // offset indexToWorld + return offsetIndexToWorld(pixelOffset); + }; + + /** + * Get a plane offset from a 3D one. + * + * @param {dwv.math.Point3D} offset3D The 3D offset. + * @returns {object} The plane offset as {x,y}. + */ + this.getPlaneOffsetFromOffset3D = function (offset3D) { + // offset worldToIndex + var pixelOffset = offsetWorldToIndex(offset3D); + // orient + var planeOffset = this.getOrientedVector3D(pixelOffset); + // make 2D + return { + x: planeOffset.getX(), + y: planeOffset.getY() + }; + }; + + /** + * Apply spacing to an offset. + * + * @param {dwv.math.Point3D} off The 3D offset. + * @returns {dwv.math.Vector3D} The world offset. + */ + function offsetIndexToWorld(off) { + return new dwv.math.Vector3D( + off.getX() * spacing.get(0), + off.getY() * spacing.get(1), + off.getZ() * spacing.get(2)); + } + + /** + * Remove spacing from an offset. + * + * @param {object} off The world offset object as {x,y,z}. + * @returns {dwv.math.Vector3D} The 3D offset. + */ + function offsetWorldToIndex(off) { + return new dwv.math.Vector3D( + off.x / spacing.get(0), + off.y / spacing.get(1), + off.z / spacing.get(2)); + } + + /** + * Orient an input vector. + * + * @param {dwv.math.Vector3D} vector The input vector. + * @returns {dwv.math.Vector3D} The oriented vector. + */ + this.getOrientedVector3D = function (vector) { + var planeVector = vector; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + planeVector = orientation.getInverse().getAbs().multiplyVector3D(vector); + } + return planeVector; + }; + + /** + * Orient an input index. + * + * @param {dwv.math.Index} index The input index. + * @returns {dwv.math.Index} The oriented index. + */ + this.getOrientedIndex = function (index) { + var planeIndex = index; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + planeIndex = orientation.getInverse().getAbs().multiplyIndex3D(index); + } + return planeIndex; + }; + + /** + * Orient an input point. + * + * @param {dwv.math.Point3D} point The input point. + * @returns {dwv.math.Point3D} The oriented point. + */ + this.getOrientedPoint = function (point) { + var planePoint = point; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + var point3D = + orientation.getInverse().getAbs().multiplyPoint3D(point.get3D()); + planePoint = point.mergeWith3D(point3D); + } + return planePoint; + }; + + /** + * De-orient an input vector. + * + * @param {dwv.math.Vector3D} planeVector The input vector. + * @returns {dwv.math.Vector3D} The de-orienteded vector. + */ + this.getDeOrientedVector3D = function (planeVector) { + var vector = planeVector; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planePoint + vector = orientation.getAbs().multiplyVector3D(planeVector); + } + return vector; + }; + + /** + * Reorder values to follow orientation. + * + * @param {object} values Values as {x,y,z}. + * @returns {object} Reoriented values as {x,y,z}. + */ + this.getOrientedXYZ = function (values) { + var orientedValues = dwv.math.getOrientedArray3D( + [ + values.x, + values.y, + values.z + ], + orientation); + return { + x: orientedValues[0], + y: orientedValues[1], + z: orientedValues[2] + }; + }; + + /** + * Reorder values to compensate for orientation. + * + * @param {object} values Values as {x,y,z}. + * @returns {object} 'Deoriented' values as {x,y,z}. + */ + this.getDeOrientedXYZ = function (values) { + var deOrientedValues = dwv.math.getDeOrientedArray3D( + [ + values.x, + values.y, + values.z + ], + orientation + ); + return { + x: deOrientedValues[0], + y: deOrientedValues[1], + z: deOrientedValues[2] + }; + }; -// pet 20 step -/* eslint-disable max-len */ -dwv.image.lut.pet_20step = { - red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], - green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 208, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 144, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], - blue: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 80, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 96, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] -}; -/* eslint-enable max-len */ + /** + * Get the scroll dimension index. + * + * @returns {number} The index. + */ + this.getScrollIndex = function () { + var index = null; + if (typeof orientation !== 'undefined') { + index = orientation.getThirdColMajorDirection(); + } else { + index = 2; + } + return index; + }; -// test -dwv.image.lut.test = { - red: dwv.image.lut.buildLut(dwv.image.lut.id), - green: dwv.image.lut.buildLut(dwv.image.lut.zero), - blue: dwv.image.lut.buildLut(dwv.image.lut.zero) }; -//red -/*dwv.image.lut.red = { - "red": dwv.image.lut.buildLut(dwv.image.lut.max), - "green": dwv.image.lut.buildLut(dwv.image.lut.id), - "blue": dwv.image.lut.buildLut(dwv.image.lut.id) -};*/ - // namespaces var dwv = dwv || {}; dwv.image = dwv.image || {}; @@ -15849,23 +17555,337 @@ dwv.image.RescaleLut = function (rsi, bitsStored) { isReady = true; }; - /** - * Get the length of the LUT array. - * - * @returns {number} The length of the LUT array. - */ - this.getLength = function () { - return length; - }; + /** + * Get the length of the LUT array. + * + * @returns {number} The length of the LUT array. + */ + this.getLength = function () { + return length; + }; + + /** + * Get the value of the LUT at the given offset. + * + * @param {number} offset The input offset in [0,2^bitsStored] range. + * @returns {number} The float32 value of the LUT at the given offset. + */ + this.getValue = function (offset) { + return lut[offset]; + }; +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Rescale Slope and Intercept + * + * @class + * @param {number} slope The slope of the RSI. + * @param {number} intercept The intercept of the RSI. + */ +dwv.image.RescaleSlopeAndIntercept = function (slope, intercept) { + /*// Check the rescale slope. + if(typeof(slope) === 'undefined') { + slope = 1; + } + // Check the rescale intercept. + if(typeof(intercept) === 'undefined') { + intercept = 0; + }*/ + + /** + * Get the slope of the RSI. + * + * @returns {number} The slope of the RSI. + */ + this.getSlope = function () { + return slope; + }; + + /** + * Get the intercept of the RSI. + * + * @returns {number} The intercept of the RSI. + */ + this.getIntercept = function () { + return intercept; + }; + + /** + * Apply the RSI on an input value. + * + * @param {number} value The input value. + * @returns {number} The value to rescale. + */ + this.apply = function (value) { + return value * slope + intercept; + }; +}; + +/** + * Check for RSI equality. + * + * @param {object} rhs The other RSI to compare to. + * @returns {boolean} True if both RSI are equal. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.equals = function (rhs) { + return rhs !== null && + this.getSlope() === rhs.getSlope() && + this.getIntercept() === rhs.getIntercept(); +}; + +/** + * Get a string representation of the RSI. + * + * @returns {string} The RSI as a string. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.toString = function () { + return (this.getSlope() + ', ' + this.getIntercept()); +}; + +/** + * Is this RSI an ID RSI. + * + * @returns {boolean} True if the RSI has a slope of 1 and no intercept. + */ +dwv.image.RescaleSlopeAndIntercept.prototype.isID = function () { + return (this.getSlope() === 1 && this.getIntercept() === 0); +}; + +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Immutable Size class. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The size values. + */ +dwv.image.Size = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create size with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create size with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val) && val !== 0; + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create size with non number or zero values.'); + } + + /** + * Get the size value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the index. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the size. + * + * @returns {string} The Size as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this index. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Size class + +/** + * Check if a dimension exists and has more than one element. + * + * @param {number} dimension The dimension to check. + * @returns {boolean} True if the size is more than one. + */ +dwv.image.Size.prototype.moreThanOne = function (dimension) { + return this.length() >= dimension + 1 && this.get(dimension) !== 1; +}; + +/** + * Check if the third direction of an orientation matrix has a size + * of more than one. + * + * @param {dwv.math.Matrix33} viewOrientation The orientation matrix. + * @returns {boolean} True if scrollable. + */ +dwv.image.Size.prototype.canScroll = function (viewOrientation) { + var dimension = 2; + if (typeof viewOrientation !== 'undefined') { + dimension = viewOrientation.getThirdColMajorDirection(); + } + return this.moreThanOne(dimension); +}; + +/** + * Get the size of a given dimension. + * + * @param {number} dimension The dimension. + * @param {number} start Optional start dimension to start counting from. + * @returns {number} The size. + */ +dwv.image.Size.prototype.getDimSize = function (dimension, start) { + if (dimension > this.length()) { + return null; + } + if (typeof start === 'undefined') { + start = 0; + } else { + if (start < 0 || start > dimension) { + throw new Error('Invalid start value for getDimSize'); + } + } + var size = 1; + for (var i = start; i < dimension; ++i) { + size *= this.get(i); + } + return size; +}; + +/** + * Get the total size. + * + * @param {number} start Optional start dimension to base the offset on. + * @returns {number} The total size. + */ +dwv.image.Size.prototype.getTotalSize = function (start) { + return this.getDimSize(this.length(), start); +}; + +/** + * Check for equality. + * + * @param {dwv.image.Size} rhs The object to compare to. + * @returns {boolean} True if both objects are equal. + */ +dwv.image.Size.prototype.equals = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + var length = this.length(); + if (length !== rhs.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Check that an index is within bounds. + * + * @param {dwv.math.Index} index The index to check. + * @returns {boolean} True if the given coordinates are within bounds. + */ +dwv.image.Size.prototype.isInBounds = function (index) { + // check input + if (!index) { + return false; + } + // check length + var length = this.length(); + if (length !== index.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (index.get(i) < 0 || index.get(i) > this.get(i) - 1) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Convert an index to an offset in memory. + * + * @param {dwv.math.Index} index The index to convert. + * @param {number} start Optional start dimension to base the offset on. + * @returns {number} The offset. + */ +dwv.image.Size.prototype.indexToOffset = function (index, start) { + // TODO check for equality + if (index.length() < this.length()) { + throw new Error('Incompatible index and size length'); + } + if (typeof start === 'undefined') { + start = 0; + } else { + if (start < 0 || start > this.length() - 1) { + throw new Error('Invalid start value for indexToOffset'); + } + } + var offset = 0; + for (var i = start; i < this.length(); ++i) { + offset += index.get(i) * this.getDimSize(i, start); + } + return offset; +}; + +/** + * Convert an offset in memory to an index. + * + * @param {number} offset The offset to convert. + * @returns {dwv.math.Index} The index. + */ +dwv.image.Size.prototype.offsetToIndex = function (offset) { + var values = new Array(this.length()); + var off = offset; + var dimSize = 0; + for (var i = this.length() - 1; i > 0; --i) { + dimSize = this.getDimSize(i); + values[i] = Math.floor(off / dimSize); + off = off - values[i] * dimSize; + } + values[0] = off; + return new dwv.math.Index(values); +}; - /** - * Get the value of the LUT at the given offset. - * - * @param {number} offset The input offset in [0,2^bitsStored] range. - * @returns {number} The float32 value of the LUT at the given offset. - */ - this.getValue = function (offset) { - return lut[offset]; +/** + * Get the 2D base of this size. + * + * @returns {object} The 2D base [0,1] as {x,y}. + */ +dwv.image.Size.prototype.get2D = function () { + return { + x: this.get(0), + y: this.get(1) }; }; @@ -15874,79 +17894,102 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; /** - * Rescale Slope and Intercept + * Immutable Spacing class. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. * * @class - * @param {number} slope The slope of the RSI. - * @param {number} intercept The intercept of the RSI. + * @param {Array} values The size values. */ -dwv.image.RescaleSlopeAndIntercept = function (slope, intercept) { - /*// Check the rescale slope. - if(typeof(slope) === 'undefined') { - slope = 1; - } - // Check the rescale intercept. - if(typeof(intercept) === 'undefined') { - intercept = 0; - }*/ +dwv.image.Spacing = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create spacing with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create spacing with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val) && val !== 0; + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create spacing with non number or zero values.'); + } /** - * Get the slope of the RSI. + * Get the spacing value at the given array index. * - * @returns {number} The slope of the RSI. + * @param {number} i The index to get. + * @returns {number} The value. */ - this.getSlope = function () { - return slope; + this.get = function (i) { + return values[i]; }; /** - * Get the intercept of the RSI. + * Get the length of the spacing. * - * @returns {number} The intercept of the RSI. + * @returns {number} The length. */ - this.getIntercept = function () { - return intercept; + this.length = function () { + return values.length; }; /** - * Apply the RSI on an input value. + * Get a string representation of the spacing. * - * @param {number} value The input value. - * @returns {number} The value to rescale. + * @returns {string} The spacing as a string. */ - this.apply = function (value) { - return value * slope + intercept; + this.toString = function () { + return '(' + values.toString() + ')'; }; -}; -/** - * Check for RSI equality. - * - * @param {object} rhs The other RSI to compare to. - * @returns {boolean} True if both RSI are equal. - */ -dwv.image.RescaleSlopeAndIntercept.prototype.equals = function (rhs) { - return rhs !== null && - this.getSlope() === rhs.getSlope() && - this.getIntercept() === rhs.getIntercept(); -}; + /** + * Get the values of this spacing. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Spacing class /** - * Get a string representation of the RSI. + * Check for equality. * - * @returns {string} The RSI as a string. + * @param {dwv.image.Spacing} rhs The object to compare to. + * @returns {boolean} True if both objects are equal. */ -dwv.image.RescaleSlopeAndIntercept.prototype.toString = function () { - return (this.getSlope() + ', ' + this.getIntercept()); +dwv.image.Spacing.prototype.equals = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + var length = this.length(); + if (length !== rhs.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; }; /** - * Is this RSI an ID RSI. + * Get the 2D base of this size. * - * @returns {boolean} True if the RSI has a slope of 1 and no intercept. + * @returns {object} The 2D base [col,row] as {x,y}. */ -dwv.image.RescaleSlopeAndIntercept.prototype.isID = function () { - return (this.getSlope() === 1 && this.getIntercept() === 0); +dwv.image.Spacing.prototype.get2D = function () { + return { + x: this.get(0), + y: this.get(1) + }; }; // namespaces @@ -15959,13 +18002,12 @@ dwv.image = dwv.image || {}; * @type {Array} */ dwv.image.viewEventNames = [ - 'slicechange', - 'framechange', - 'wlwidthchange', - 'wlcenterchange', + 'wlchange', 'wlpresetadd', 'colourchange', - 'positionchange' + 'positionchange', + 'opacitychange', + 'alphafuncchange' ]; /** @@ -15977,6 +18019,22 @@ dwv.image.viewEventNames = [ * (either directly or with helper methods). */ dwv.image.View = function (image) { + // closure to self + var self = this; + + // listen to appendframe event to update the current position + // to add the extra dimension + image.addEventListener('appendframe', function () { + // update current position if first appendFrame + var position = self.getCurrentPosition(); + if (position.length() === 3) { + // add dimension + var values = position.getValues(); + values.push(0); + self.setCurrentPosition(new dwv.math.Point(values)); + } + }); + /** * Window lookup tables, indexed per Rescale Slope and Intercept (RSI). * @@ -16018,19 +18076,27 @@ dwv.image.View = function (image) { */ var colourMap = dwv.image.lut.plain; /** - * Current position. + * Current position as a Point3D. * * @private * @type {object} */ var currentPosition = null; /** - * Current frame. Zero based. + * View orientation. Undefined will use the original slice ordering. * * @private - * @type {number} + * @type {object} + */ + var orientation; + + /** + * Listener handler. + * + * @type {object} + * @private */ - var currentFrame = null; + var listenerHandler = new dwv.utils.ListenerHandler(); /** * Get the associated image. @@ -16049,13 +18115,39 @@ dwv.image.View = function (image) { image = inImage; }; + /** + * Get the view orientation. + * + * @returns {dwv.math.Matrix33} The orientation matrix. + */ + this.getOrientation = function () { + return orientation; + }; + + /** + * Set the view orientation. + * + * @param {dwv.math.Matrix33} mat33 The orientation matrix. + */ + this.setOrientation = function (mat33) { + orientation = mat33; + }; + /** * Set initial position. */ this.setInitialPosition = function () { var silent = true; - this.setCurrentPosition({i: 0, j: 0, k: 0}, silent); - this.setCurrentFrame(0, silent); + + var geometry = image.getGeometry(); + var values = new Array(geometry.getSize().length()); + values.fill(0); + var index = new dwv.math.Index(values); + + this.setCurrentPosition( + geometry.indexToWorld(index), + silent + ); }; /** @@ -16073,6 +18165,46 @@ dwv.image.View = function (image) { return Math.round(1000 / recommendedDisplayFrameRate); }; + /** + * Per value alpha function. + * + * @param {*} _value The pixel value. Can be a number for monochrome + * data or an array for RGB data. + * @returns {number} The coresponding alpha [0,255]. + */ + var alphaFunction = function (_value) { + // default always returns fully visible + return 0xff; + }; + + /** + * Get the alpha function. + * + * @returns {Function} The function. + */ + this.getAlphaFunction = function () { + return alphaFunction; + }; + + /** + * Set alpha function. + * + * @param {Function} func The function. + * @fires dwv.image.View#alphafuncchange + */ + this.setAlphaFunction = function (func) { + alphaFunction = func; + /** + * Alpha func change event. + * + * @event dwv.image.View#alphafuncchange + * @type {object} + */ + fireEvent({ + type: 'alphafuncchange' + }); + }; + /** * Get the window LUT of the image. * Warning: can be undefined in no window/level was set. @@ -16080,18 +18212,17 @@ dwv.image.View = function (image) { * @param {object} rsi Optional image rsi, will take the one of the * current slice otherwise. * @returns {Window} The window LUT of the image. - * @fires dwv.image.View#wlwidthchange - * @fires dwv.image.View#wlcenterchange + * @fires dwv.image.View#wlchange */ this.getCurrentWindowLut = function (rsi) { - // check position (also sets frame) + // check position if (!this.getCurrentPosition()) { this.setInitialPosition(); } - var sliceNumber = this.getCurrentPosition().k; + var currentIndex = this.getCurrentIndex(); // use current rsi if not provided if (typeof rsi === 'undefined') { - rsi = image.getRescaleSlopeAndIntercept(sliceNumber); + rsi = image.getRescaleSlopeAndIntercept(currentIndex); } // get the current window level @@ -16102,7 +18233,8 @@ dwv.image.View = function (image) { typeof windowPresets[currentPresetName].perslice !== 'undefined' && windowPresets[currentPresetName].perslice === true) { // get the preset for this slice - wl = windowPresets[currentPresetName].wl[sliceNumber]; + var offset = image.getSecondaryOffset(currentIndex); + wl = windowPresets[currentPresetName].wl[offset]; } // regular case if (!wl) { @@ -16134,19 +18266,12 @@ dwv.image.View = function (image) { wlut.setWindowLevel(wl); wlut.update(); // fire change event - if (!lutWl || lutWl.getWidth() !== wl.getWidth()) { - this.fireEvent({ - type: 'wlwidthchange', - value: [wl.getWidth()], - wc: wl.getCenter(), - ww: wl.getWidth(), - skipGenerate: true - }); - } - if (!lutWl || lutWl.getCenter() !== wl.getCenter()) { - this.fireEvent({ - type: 'wlcenterchange', - value: [wl.getCenter()], + if (!lutWl || + lutWl.getWidth() !== wl.getWidth() || + lutWl.getCenter() !== wl.getCenter()) { + fireEvent({ + type: 'wlchange', + value: [wl.getCenter(), wl.getWidth()], wc: wl.getCenter(), ww: wl.getWidth(), skipGenerate: true @@ -16207,18 +18332,16 @@ dwv.image.View = function (image) { * Add window presets to the existing ones. * * @param {object} presets The window presets. - * @param {number} k The slice the preset belong to. */ - this.addWindowPresets = function (presets, k) { + this.addWindowPresets = function (presets) { var keys = Object.keys(presets); var key = null; for (var i = 0; i < keys.length; ++i) { key = keys[i]; if (typeof windowPresets[key] !== 'undefined') { if (typeof windowPresets[key].perslice !== 'undefined' && - windowPresets[key].perslice === true) { - // use first new preset wl... - windowPresets[key].wl.splice(k, 0, presets[key].wl[0]); + windowPresets[key].perslice === true) { + throw new Error('Cannot add perslice preset'); } else { windowPresets[key] = presets[key]; } @@ -16233,7 +18356,7 @@ dwv.image.View = function (image) { * @type {object} * @property {string} name The name of the preset. */ - this.fireEvent({ + fireEvent({ type: 'wlpresetadd', name: key }); @@ -16266,7 +18389,7 @@ dwv.image.View = function (image) { * @property {number} wc The new window center value. * @property {number} ww The new window wdth value. */ - this.fireEvent({ + fireEvent({ type: 'colourchange', wc: this.getCurrentWindowLut().getWindowLevel().getCenter(), ww: this.getCurrentWindowLut().getWindowLevel().getWidth() @@ -16276,101 +18399,102 @@ dwv.image.View = function (image) { /** * Get the current position. * - * @returns {object} The current position. + * @returns {dwv.math.Point} The current position. */ this.getCurrentPosition = function () { - // return a clone to avoid reference problems - return currentPosition ? { - i: currentPosition.i, - j: currentPosition.j, - k: currentPosition.k - } : null; + return currentPosition; + }; + + /** + * Get the current index. + * + * @returns {dwv.math.Index} The current index. + */ + this.getCurrentIndex = function () { + var geometry = this.getImage().getGeometry(); + return geometry.worldToIndex(currentPosition); }; + /** * Set the current position. * - * @param {object} pos The current position. - * @param {boolean} silent If true, does not fire a slicechange event. + * @param {dwv.math.Point} newPosition The new position. + * @param {boolean} silent Flag to fire event or not. * @returns {boolean} False if not in bounds - * @fires dwv.image.View#slicechange * @fires dwv.image.View#positionchange */ - this.setCurrentPosition = function (pos, silent) { + this.setCurrentPosition = function (newPosition, silent) { // check input if (typeof silent === 'undefined') { silent = false; } // check if possible - if (!image.getGeometry().getSize().isInBounds(pos.i, pos.j, pos.k)) { + var geometry = image.getGeometry(); + if (!geometry.isInBounds(newPosition)) { return false; } - // check if new - var equalPos = function (pos1, pos2) { - return pos2 !== null && - pos1.i === pos2.i && - pos1.j === pos2.j && - pos1.k === pos2.k; - }; - var isNew = !equalPos(pos, currentPosition); + + var isNew = !currentPosition || !currentPosition.equals(newPosition); if (isNew) { - var isNewSlice = currentPosition - ? pos.k !== currentPosition.k : true; + var posIndex = geometry.worldToIndex(newPosition); + var diffDims = null; + if (currentPosition) { + if (currentPosition.canCompare(newPosition)) { + diffDims = currentPosition.compare(newPosition); + } else { + diffDims = []; + var minLen = Math.min(currentPosition.length(), newPosition.length()); + for (var i = 0; i < minLen; ++i) { + if (currentPosition.get(i) !== newPosition.get(i)) { + diffDims.push(i); + } + } + var maxLen = Math.max(currentPosition.length(), newPosition.length()); + for (var j = minLen; j < maxLen; ++j) { + diffDims.push(j); + } + } + } else { + diffDims = []; + for (var k = 0; k < newPosition.length(); ++k) { + diffDims.push(k); + } + } + // assign - currentPosition = pos; + currentPosition = newPosition; - // fire a 'positionchange' event - if (image.getPhotometricInterpretation().match(/MONOCHROME/) !== null) { - var pixValue = image.getRescaledValue( - pos.i, pos.j, pos.k, this.getCurrentFrame()); + if (!silent) { /** * Position change event. * * @event dwv.image.View#positionchange * @type {object} - * @property {Array} value The changed value. - * @property {number} i The new column position - * @property {number} j The new row position - * @property {number} k The new slice position - * @property {object} pixelValue The image value at the new position, - * (can be undefined). + * @property {Array} value The changed value as [index, pixelValue]. + * @property {Array} diffDims An array of modified indices. */ - this.fireEvent({ - type: 'positionchange', - value: [pos.i, pos.j, pos.k, pixValue], - i: pos.i, - j: pos.j, - k: pos.k, - pixelValue: pixValue - }); - } else { - this.fireEvent({ + var posEvent = { type: 'positionchange', - value: [pos.i, pos.j, pos.k], - i: pos.i, - j: pos.j, - k: pos.k - }); - } - - // fire a slice change event (used to trigger redraw) - if (!silent && isNewSlice) { - /** - * Slice change event. - * - * @event dwv.image.View#slicechange - * @type {object} - * @property {Array} value The changed value. - * @property {object} data Associated event data: the imageUid. - */ - this.fireEvent({ - type: 'slicechange', - value: [currentPosition.k], + value: [ + posIndex.getValues(), + currentPosition.getValues(), + ], + diffDims: diffDims, data: { - imageUid: image.getImageUids()[currentPosition.k] + imageUid: image.getImageUid(posIndex) } - }); + }; + + // add value if possible + if (image.canQuantify()) { + var pixValue = image.getRescaledValueAtIndex(posIndex); + posEvent.value.push(pixValue); + } + + // fire + fireEvent(posEvent); } } @@ -16379,59 +18503,15 @@ dwv.image.View = function (image) { }; /** - * Get the current frame number. + * Set the current index. * - * @returns {number} The current frame number. - */ - this.getCurrentFrame = function () { - return currentFrame; - }; - - /** - * Set the current frame number. - * - * @param {number} frame The current frame number. - * @param {boolean} silent Flag to launch events with skipGenerate. - * @returns {boolean} False if not in bounds - * @fires dwv.image.View#framechange + * @param {dwv.math.Index} index The index. + * @param {boolean} silent If true, does not fire a positionchange event. + * @returns {boolean} False if not in bounds. */ - this.setCurrentFrame = function (frame, silent) { - // check input - if (typeof silent === 'undefined') { - silent = false; - } - - // check if possible - if (frame < 0 || frame >= image.getNumberOfFrames()) { - return false; - } - // check if new - var isNew = currentFrame !== frame; - - if (isNew) { - // assign - currentFrame = frame; - // fire event for multi frame data - if (image.getNumberOfFrames() !== 1) { - /** - * Frame change event. - * - * @event dwv.image.View#framechange - * @type {object} - * @property {Array} value The changed value. - * @property {number} frame The new frame number - * @property {boolean} skipGenerate Flag to skip view generation. - */ - this.fireEvent({ - type: 'framechange', - value: [currentFrame], - frame: currentFrame, - skipGenerate: silent - }); - } - } - // all good - return true; + this.setCurrentIndex = function (index, silent) { + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(index), silent); }; /** @@ -16442,8 +18522,7 @@ dwv.image.View = function (image) { * @param {string} name Associated preset name, defaults to 'manual'. * Warning: uses the latest set rescale LUT or the default linear one. * @param {boolean} silent Flag to launch events with skipGenerate. - * @fires dwv.image.View#wlwidthchange - * @fires dwv.image.View#wlcenterchange + * @fires dwv.image.View#wlchange */ this.setWindowLevel = function (center, width, name, silent) { // window width shall be >= 1 (see https://www.dabsoft.ch/dicom/3/C.11.2.1.2/) @@ -16473,40 +18552,20 @@ dwv.image.View = function (image) { currentWl = newWl; currentPresetName = name; - if (isNewWidth) { - /** - * Window/level width change event. - * - * @event dwv.image.View#wlwidthchange - * @type {object} - * @property {Array} value The changed value. - * @property {number} wc The new window center value. - * @property {number} ww The new window wdth value. - * @property {boolean} skipGenerate Flag to skip view generation. - */ - this.fireEvent({ - type: 'wlwidthchange', - value: [width], - wc: center, - ww: width, - skipGenerate: silent - }); - } - - if (isNewCenter) { + if (isNewWidth || isNewCenter) { /** - * Window/level center change event. + * Window/level change event. * - * @event dwv.image.View#wlcenterchange + * @event dwv.image.View#wlchange * @type {object} * @property {Array} value The changed value. * @property {number} wc The new window center value. * @property {number} ww The new window wdth value. * @property {boolean} skipGenerate Flag to skip view generation. */ - this.fireEvent({ - type: 'wlcenterchange', - value: [center], + fireEvent({ + type: 'wlchange', + value: [center, width], wc: center, ww: width, skipGenerate: silent @@ -16528,16 +18587,19 @@ dwv.image.View = function (image) { } // special min/max if (name === 'minmax' && typeof preset.wl === 'undefined') { - preset.wl = this.getWindowLevelMinMax(); + preset.wl = [this.getWindowLevelMinMax()]; } - // special 'perslice' case + // default to first + var wl = preset.wl[0]; + // check if 'perslice' case if (typeof preset.perslice !== 'undefined' && preset.perslice === true) { - preset = {wl: preset.wl[this.getCurrentPosition().k]}; + var offset = image.getSecondaryOffset(this.getCurrentIndex()); + wl = preset.wl[offset]; } // set w/l this.setWindowLevel( - preset.wl.getCenter(), preset.wl.getWidth(), name, silent); + wl.getCenter(), wl.getWidth(), name, silent); }; /** @@ -16566,28 +18628,36 @@ dwv.image.View = function (image) { }; /** - * View listeners + * Add an event listener to this class. * - * @private - * @type {object} + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. */ - var listeners = {}; + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** - * Get the view listeners. + * Remove an event listener from this class. * - * @returns {object} The view listeners. + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. */ - this.getListeners = function () { - return listeners; + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); }; + /** - * Set the view listeners. + * Fire an event: call all associated listeners with the input event object. * - * @param {object} list The view listeners. + * @param {object} event The event to fire. + * @private */ - this.setListeners = function (list) { - listeners = list; - }; + function fireEvent(event) { + listenerHandler.fireEvent(event); + } }; /** @@ -16627,14 +18697,14 @@ dwv.image.View.prototype.setWindowLevelMinMax = function () { * @param {Array} array The array to fill in. */ dwv.image.View.prototype.generateImageData = function (array) { - // check position (also sets frame) + // check position if (!this.getCurrentPosition()) { this.setInitialPosition(); } - var position = this.getCurrentPosition(); - var frame = this.getCurrentFrame(); var image = this.getImage(); - var iterator = dwv.image.getSliceIterator(this.getImage(), position.k, frame); + var position = this.getCurrentIndex(); + var iterator = dwv.image.getSliceIterator( + image, position, false, this.getOrientation()); var photoInterpretation = image.getPhotometricInterpretation(); switch (photoInterpretation) { @@ -16643,6 +18713,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataMonochrome( array, iterator, + this.getAlphaFunction(), this.getCurrentWindowLut(), this.getColourMap() ); @@ -16652,6 +18723,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataPaletteColor( array, iterator, + this.getAlphaFunction(), this.getColourMap(), image.getMeta().BitsStored === 16 ); @@ -16661,6 +18733,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataRgb( array, iterator, + this.getAlphaFunction(), this.getCurrentWindowLut() ); break; @@ -16668,7 +18741,8 @@ dwv.image.View.prototype.generateImageData = function (array) { case 'YBR_FULL': dwv.image.generateImageDataYbrFull( array, - iterator + iterator, + this.getAlphaFunction() ); break; @@ -16679,50 +18753,83 @@ dwv.image.View.prototype.generateImageData = function (array) { }; /** - * Add an event listener on the view. + * Increment the provided dimension. * - * @param {string} type The event type. - * @param {object} listener The method associated with the provided event type. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. */ -dwv.image.View.prototype.addEventListener = function (type, listener) { - var listeners = this.getListeners(); - if (!listeners[type]) { - listeners[type] = []; +dwv.image.View.prototype.incrementIndex = function (dim, silent) { + var index = this.getCurrentIndex(); + var values = new Array(index.length()); + values.fill(0); + if (dim < values.length) { + values[dim] = 1; + } else { + console.warn('Cannot increment given index: ', dim, values.length); } - listeners[type].push(listener); + var incr = new dwv.math.Index(values); + var newIndex = index.add(incr); + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent); }; /** - * Remove an event listener on the view. + * Decrement the provided dimension. * - * @param {string} type The event type. - * @param {object} listener The method associated with the provided event type. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. */ -dwv.image.View.prototype.removeEventListener = function (type, listener) { - var listeners = this.getListeners(); - if (!listeners[type]) { - return; - } - for (var i = 0; i < listeners[type].length; ++i) { - if (listeners[type][i] === listener) { - listeners[type].splice(i, 1); - } +dwv.image.View.prototype.decrementIndex = function (dim, silent) { + var index = this.getCurrentIndex(); + var values = new Array(index.length()); + values.fill(0); + if (dim < values.length) { + values[dim] = -1; + } else { + console.warn('Cannot decrement given index: ', dim, values.length); } + var incr = new dwv.math.Index(values); + var newIndex = index.add(incr); + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent); }; /** - * Fire an event: call all associated listeners. + * Get the scroll dimension index. * - * @param {object} event The event to fire. + * @returns {number} The index. */ -dwv.image.View.prototype.fireEvent = function (event) { - var listeners = this.getListeners(); - if (!listeners[event.type]) { - return; - } - for (var i = 0; i < listeners[event.type].length; ++i) { - listeners[event.type][i](event); +dwv.image.View.prototype.getScrollIndex = function () { + var index = null; + var orientation = this.getOrientation(); + if (typeof orientation !== 'undefined') { + index = orientation.getThirdColMajorDirection(); + } else { + index = 2; } + return index; +}; + +/** + * Decrement the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ +dwv.image.View.prototype.decrementScrollIndex = function (silent) { + return this.decrementIndex(this.getScrollIndex(), silent); +}; + +/** + * Increment the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ +dwv.image.View.prototype.incrementScrollIndex = function (silent) { + return this.incrementIndex(this.getScrollIndex(), silent); }; // namespaces @@ -16730,17 +18837,24 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; /** - * View factory. + * {@link dwv.image.View} factory. * * @class */ dwv.image.ViewFactory = function () {}; +/** + * {@link dwv.image.View} factory. Defaults to local one. + * + * @see dwv.image.ViewFactory + */ +dwv.ViewFactory = dwv.image.ViewFactory; + /** * Get an View object from the read DICOM file. * * @param {object} dicomElements The DICOM tags. - * @param {object} image The associated image. + * @param {dwv.image.Image} image The associated image. * @returns {dwv.image.View} The new View. */ dwv.image.ViewFactory.prototype.create = function (dicomElements, image) { @@ -16800,12 +18914,14 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. * @param {object} windowLut The window/level LUT. * @param {object} colourMap The colour map. */ dwv.image.generateImageDataMonochrome = function ( array, iterator, + alphaFunc, windowLut, colourMap) { var index = 0; @@ -16818,7 +18934,7 @@ dwv.image.generateImageDataMonochrome = function ( array.data[index] = colourMap.red[pxValue]; array.data[index + 1] = colourMap.green[pxValue]; array.data[index + 2] = colourMap.blue[pxValue]; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); @@ -16834,12 +18950,14 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. * @param {object} colourMap The colour map. * @param {boolean} is16BitsStored Flag to know if the data is 16bits. */ dwv.image.generateImageDataPaletteColor = function ( array, iterator, + alphaFunc, colourMap, is16BitsStored) { // right shift 8 @@ -16868,7 +18986,7 @@ dwv.image.generateImageDataPaletteColor = function ( array.data[index + 1] = colourMap.green[pxValue]; array.data[index + 2] = colourMap.blue[pxValue]; } - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(pxValue); // increment index += 4; ival = iterator.next(); @@ -16884,10 +19002,12 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. */ dwv.image.generateImageDataRgb = function ( array, - iterator) { + iterator, + alphaFunc) { var index = 0; var ival = iterator.next(); while (!ival.done) { @@ -16895,7 +19015,7 @@ dwv.image.generateImageDataRgb = function ( array.data[index] = ival.value[0]; array.data[index + 1] = ival.value[1]; array.data[index + 2] = ival.value[2]; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); @@ -16911,10 +19031,12 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. */ dwv.image.generateImageDataYbrFull = function ( array, - iterator) { + iterator, + alphaFunc) { var index = 0; var rgb = null; var ival = iterator.next(); @@ -16926,7 +19048,7 @@ dwv.image.generateImageDataYbrFull = function ( array.data[index] = rgb.r; array.data[index + 1] = rgb.g; array.data[index + 2] = rgb.b; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); @@ -17342,10 +19464,8 @@ dwv.io.DicomDataLoader = function () { this.load = function (buffer, origin, index) { // setup db2v ony once if (!isLoading) { - // set character set - if (typeof options.defaultCharacterSet !== 'undefined') { - db2v.setDefaultCharacterSet(options.defaultCharacterSet); - } + // pass options + db2v.setOptions(options); // connect handlers db2v.onloadstart = self.onloadstart; db2v.onprogress = self.onprogress; @@ -17742,6 +19862,7 @@ dwv.io.FilesLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: this.getDefaultCharacterSet() }); // set loader callbacks @@ -17765,7 +19886,7 @@ dwv.io.FilesLoader = function () { } } if (!foundLoader) { - throw new Error('No loader found for file: ' + dataElement); + throw new Error('No loader found for file: ' + dataElement.name); } var getLoadHandler = function (loader, dataElement, i) { @@ -18265,6 +20386,7 @@ dwv.io.MemoryLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: this.getDefaultCharacterSet() }); // set loader callbacks @@ -18428,62 +20550,266 @@ dwv.io.RawImageLoader = function () { } /** - * Load data. + * Load data. + * + * @param {object} buffer The read data. + * @param {string} origin The data origin. + * @param {number} index The data index. + */ + this.load = function (buffer, origin, index) { + aborted = false; + // create a DOM image + var image = new Image(); + // triggered by ctx.drawImage + image.onload = function (/*event*/) { + try { + if (!aborted) { + self.onprogress({ + lengthComputable: true, + loaded: 100, + total: 100, + index: index, + source: origin + }); + self.onload(dwv.image.getViewFromDOMImage(this, origin)); + } + } catch (error) { + self.onerror({ + error: error, + source: origin + }); + } finally { + self.onloadend({ + source: origin + }); + } + }; + // storing values to pass them on + image.origin = origin; + image.index = index; + if (typeof origin === 'string') { + // url case + var ext = origin.split('.').pop().toLowerCase(); + image.src = createDataUri(buffer, ext); + } else { + image.src = buffer; + } + }; + + /** + * Abort load. + */ + this.abort = function () { + aborted = true; + self.onabort({}); + self.onloadend({}); + }; + +}; // class RawImageLoader + +/** + * Check if the loader can load the provided file. + * + * @param {object} file The file to check. + * @returns {boolean} True if the file can be loaded. + */ +dwv.io.RawImageLoader.prototype.canLoadFile = function (file) { + return file.type.match('image.*'); +}; + +/** + * Check if the loader can load the provided url. + * + * @param {string} url The url to check. + * @returns {boolean} True if the url can be loaded. + */ +dwv.io.RawImageLoader.prototype.canLoadUrl = function (url) { + var urlObjext = dwv.utils.getUrlFromUri(url); + // extension + var ext = dwv.utils.getFileExtension(urlObjext.pathname); + var hasImageExt = (ext === 'jpeg') || (ext === 'jpg') || + (ext === 'png') || (ext === 'gif'); + // content type (for wado url) + var contentType = urlObjext.searchParams.get('contentType'); + var hasContentType = contentType !== null && + typeof contentType !== 'undefined'; + var hasImageContentType = (contentType === 'image/jpeg') || + (contentType === 'image/png') || + (contentType === 'image/gif'); + + return hasContentType ? hasImageContentType : hasImageExt; +}; + +/** + * Get the file content type needed by the loader. + * + * @returns {number} One of the 'dwv.io.fileContentTypes'. + */ +dwv.io.RawImageLoader.prototype.loadFileAs = function () { + return dwv.io.fileContentTypes.DataURL; +}; + +/** + * Get the url content type needed by the loader. + * + * @returns {number} One of the 'dwv.io.urlContentTypes'. + */ +dwv.io.RawImageLoader.prototype.loadUrlAs = function () { + return dwv.io.urlContentTypes.ArrayBuffer; +}; + +/** + * Handle a load start event. + * Default does nothing. + * + * @param {object} _event The load start event. + */ +dwv.io.RawImageLoader.prototype.onloadstart = function (_event) {}; +/** + * Handle a progress event. + * Default does nothing. + * + * @param {object} _event The progress event. + */ +dwv.io.RawImageLoader.prototype.onprogress = function (_event) {}; +/** + * Handle a load event. + * Default does nothing. + * + * @param {object} _event The load event fired + * when a file has been loaded successfully. + */ +dwv.io.RawImageLoader.prototype.onload = function (_event) {}; +/** + * Handle an load end event. + * Default does nothing. + * + * @param {object} _event The load end event fired + * when a file load has completed, successfully or not. + */ +dwv.io.RawImageLoader.prototype.onloadend = function (_event) {}; +/** + * Handle an error event. + * Default does nothing. + * + * @param {object} _event The error event. + */ +dwv.io.RawImageLoader.prototype.onerror = function (_event) {}; +/** + * Handle an abort event. + * Default does nothing. + * + * @param {object} _event The abort event. + */ +dwv.io.RawImageLoader.prototype.onabort = function (_event) {}; + +/** + * Add to Loader list. + */ +dwv.io.loaderList = dwv.io.loaderList || []; +dwv.io.loaderList.push('RawImageLoader'); + +// namespaces +var dwv = dwv || {}; +dwv.io = dwv.io || {}; + +/** + * Raw video loader. + * url example (cors enabled): + * https://raw.githubusercontent.com/clappr/clappr/master/test/fixtures/SampleVideo_360x240_1mb.mp4 + * + * @class + */ +dwv.io.RawVideoLoader = function () { + // closure to self + var self = this; + + /** + * Set the loader options. + * + * @param {object} _opt The input options. + */ + this.setOptions = function (_opt) { + // does nothing + }; + + /** + * Is the load ongoing? TODO... + * + * @returns {boolean} True if loading. + */ + this.isLoading = function () { + return true; + }; + + /** + * Create a Data URI from an HTTP request response. + * + * @param {object} response The HTTP request response. + * @param {string} dataType The data type. + * @returns {string} The data URI. + * @private + */ + function createDataUri(response, dataType) { + // image data as string + var bytes = new Uint8Array(response); + var videoDataStr = ''; + for (var i = 0; i < bytes.byteLength; ++i) { + videoDataStr += String.fromCharCode(bytes[i]); + } + // create uri + var uri = 'data:video/' + dataType + ';base64,' + window.btoa(videoDataStr); + return uri; + } + + /** + * Internal Data URI load. * * @param {object} buffer The read data. * @param {string} origin The data origin. * @param {number} index The data index. */ this.load = function (buffer, origin, index) { - aborted = false; - // create a DOM image - var image = new Image(); - // triggered by ctx.drawImage - image.onload = function (/*event*/) { + // create a DOM video + var video = document.createElement('video'); + if (typeof origin === 'string') { + // url case + var ext = origin.split('.').pop().toLowerCase(); + video.src = createDataUri(buffer, ext); + } else { + video.src = buffer; + } + // storing values to pass them on + video.file = origin; + video.index = index; + // onload handler + video.onloadedmetadata = function (/*event*/) { try { - if (!aborted) { - self.onprogress({ - lengthComputable: true, - loaded: 100, - total: 100, - index: index, - source: origin - }); - self.onload(dwv.image.getViewFromDOMImage(this, origin)); - } + dwv.image.getViewFromDOMVideo(this, + self.onloaditem, self.onload, + self.onprogress, self.onloadend, + index, origin); } catch (error) { self.onerror({ error: error, source: origin }); - } finally { self.onloadend({ source: origin }); } }; - // storing values to pass them on - image.origin = origin; - image.index = index; - if (typeof origin === 'string') { - // url case - var ext = origin.split('.').pop().toLowerCase(); - image.src = createDataUri(buffer, ext); - } else { - image.src = buffer; - } }; /** * Abort load. */ this.abort = function () { - aborted = true; self.onabort({}); self.onloadend({}); }; -}; // class RawImageLoader +}; // class RawVideoLoader /** * Check if the loader can load the provided file. @@ -18491,8 +20817,8 @@ dwv.io.RawImageLoader = function () { * @param {object} file The file to check. * @returns {boolean} True if the file can be loaded. */ -dwv.io.RawImageLoader.prototype.canLoadFile = function (file) { - return file.type.match('image.*'); +dwv.io.RawVideoLoader.prototype.canLoadFile = function (file) { + return file.type.match('video.*'); }; /** @@ -18501,21 +20827,11 @@ dwv.io.RawImageLoader.prototype.canLoadFile = function (file) { * @param {string} url The url to check. * @returns {boolean} True if the url can be loaded. */ -dwv.io.RawImageLoader.prototype.canLoadUrl = function (url) { +dwv.io.RawVideoLoader.prototype.canLoadUrl = function (url) { var urlObjext = dwv.utils.getUrlFromUri(url); - // extension var ext = dwv.utils.getFileExtension(urlObjext.pathname); - var hasImageExt = (ext === 'jpeg') || (ext === 'jpg') || - (ext === 'png') || (ext === 'gif'); - // content type (for wado url) - var contentType = urlObjext.searchParams.get('contentType'); - var hasContentType = contentType !== null && - typeof contentType !== 'undefined'; - var hasImageContentType = (contentType === 'image/jpeg') || - (contentType === 'image/png') || - (contentType === 'image/gif'); - - return hasContentType ? hasImageContentType : hasImageExt; + return (ext === 'mp4') || (ext === 'ogg') || + (ext === 'webm'); }; /** @@ -18523,7 +20839,7 @@ dwv.io.RawImageLoader.prototype.canLoadUrl = function (url) { * * @returns {number} One of the 'dwv.io.fileContentTypes'. */ -dwv.io.RawImageLoader.prototype.loadFileAs = function () { +dwv.io.RawVideoLoader.prototype.loadFileAs = function () { return dwv.io.fileContentTypes.DataURL; }; @@ -18532,7 +20848,7 @@ dwv.io.RawImageLoader.prototype.loadFileAs = function () { * * @returns {number} One of the 'dwv.io.urlContentTypes'. */ -dwv.io.RawImageLoader.prototype.loadUrlAs = function () { +dwv.io.RawVideoLoader.prototype.loadUrlAs = function () { return dwv.io.urlContentTypes.ArrayBuffer; }; @@ -18542,22 +20858,30 @@ dwv.io.RawImageLoader.prototype.loadUrlAs = function () { * * @param {object} _event The load start event. */ -dwv.io.RawImageLoader.prototype.onloadstart = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onloadstart = function (_event) {}; /** * Handle a progress event. * Default does nothing. * * @param {object} _event The progress event. */ -dwv.io.RawImageLoader.prototype.onprogress = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onprogress = function (_event) {}; +/** + * Handle a load item event. + * Default does nothing. + * + * @param {object} _event The load item event fired + * when a file item has been loaded successfully. + */ +dwv.io.RawVideoLoader.prototype.onloaditem = function (_event) {}; /** * Handle a load event. * Default does nothing. * * @param {object} _event The load event fired - * when a file has been loaded successfully. + * when a file has been loaded successfully. */ -dwv.io.RawImageLoader.prototype.onload = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onload = function (_event) {}; /** * Handle an load end event. * Default does nothing. @@ -18565,229 +20889,552 @@ dwv.io.RawImageLoader.prototype.onload = function (_event) {}; * @param {object} _event The load end event fired * when a file load has completed, successfully or not. */ -dwv.io.RawImageLoader.prototype.onloadend = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onloadend = function (_event) {}; /** * Handle an error event. * Default does nothing. * * @param {object} _event The error event. */ -dwv.io.RawImageLoader.prototype.onerror = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onerror = function (_event) {}; /** * Handle an abort event. * Default does nothing. * * @param {object} _event The abort event. */ -dwv.io.RawImageLoader.prototype.onabort = function (_event) {}; +dwv.io.RawVideoLoader.prototype.onabort = function (_event) {}; /** * Add to Loader list. */ dwv.io.loaderList = dwv.io.loaderList || []; -dwv.io.loaderList.push('RawImageLoader'); +dwv.io.loaderList.push('RawVideoLoader'); // namespaces var dwv = dwv || {}; dwv.io = dwv.io || {}; +// external +var Konva = Konva || {}; /** - * Raw video loader. - * url example (cors enabled): - * https://raw.githubusercontent.com/clappr/clappr/master/test/fixtures/SampleVideo_360x240_1mb.mp4 + * State class. + * Saves: data url/path, display info. + * + * History: + * - v0.5 (dwv 0.30.0, ??/2021) + * - store position as array + * - new draw position group key + * - v0.4 (dwv 0.29.0, 06/2021) + * - move drawing details into meta property + * - remove scale center and translation, add offset + * - v0.3 (dwv v0.23.0, 03/2018) + * - new drawing structure, drawings are now the full layer object and + * using toObject to avoid saving a string representation + * - new details structure: simple array of objects referenced by draw ids + * - v0.2 (dwv v0.17.0, 12/2016) + * - adds draw details: array [nslices][nframes] of detail objects + * - v0.1 (dwv v0.15.0, 07/2016) + * - adds version + * - drawings: array [nslices][nframes] with all groups + * - initial release (dwv v0.10.0, 05/2015), no version number... + * - content: window-center, window-width, position, scale, + * scaleCenter, translation, drawings + * - drawings: array [nslices] with all groups * * @class */ -dwv.io.RawVideoLoader = function () { - // closure to self - var self = this; - +dwv.io.State = function () { /** - * Set the loader options. + * Save the application state as JSON. * - * @param {object} _opt The input options. + * @param {object} app The associated application. + * @returns {string} The state as a JSON string. */ - this.setOptions = function (_opt) { - // does nothing + this.toJSON = function (app) { + var layerGroup = app.getActiveLayerGroup(); + var viewController = + layerGroup.getActiveViewLayer().getViewController(); + var drawLayer = layerGroup.getActiveDrawLayer(); + var position = viewController.getCurrentPosition(); + // return a JSON string + return JSON.stringify({ + version: '0.5', + 'window-center': viewController.getWindowLevel().center, + 'window-width': viewController.getWindowLevel().width, + position: [position.getX(), position.getY(), position.getZ()], + scale: app.getAddedScale(), + offset: app.getOffset(), + drawings: drawLayer.getKonvaLayer().toObject(), + drawingsDetails: app.getDrawStoreDetails() + }); }; - /** - * Is the load ongoing? TODO... + * Load an application state from JSON. * - * @returns {boolean} True if loading. + * @param {string} json The JSON representation of the state. + * @returns {object} The state object. */ - this.isLoading = function () { - return true; + this.fromJSON = function (json) { + var data = JSON.parse(json); + var res = null; + if (data.version === '0.1') { + res = readV01(data); + } else if (data.version === '0.2') { + res = readV02(data); + } else if (data.version === '0.3') { + res = readV03(data); + } else if (data.version === '0.4') { + res = readV04(data); + } else if (data.version === '0.5') { + res = readV05(data); + } else { + throw new Error('Unknown state file format version: \'' + + data.version + '\'.'); + } + return res; }; - /** - * Create a Data URI from an HTTP request response. + * Load an application state from JSON. * - * @param {object} response The HTTP request response. - * @param {string} dataType The data type. - * @returns {string} The data URI. - * @private + * @param {object} app The app to apply the state to. + * @param {object} data The state data. */ - function createDataUri(response, dataType) { - // image data as string - var bytes = new Uint8Array(response); - var videoDataStr = ''; - for (var i = 0; i < bytes.byteLength; ++i) { - videoDataStr += String.fromCharCode(bytes[i]); + this.apply = function (app, data) { + var layerGroup = app.getActiveLayerGroup(); + var viewController = + layerGroup.getActiveViewLayer().getViewController(); + // display + viewController.setWindowLevel( + data['window-center'], data['window-width']); + viewController.setCurrentPosition( + new dwv.math.Point3D( + data.position[0], data.position[1], data.position[2]), true); + // apply saved scale on top of current base one + var baseScale = app.getActiveLayerGroup().getBaseScale(); + var scale = null; + var offset = null; + if (typeof data.scaleCenter !== 'undefined') { + scale = { + x: data.scale * baseScale.x, + y: data.scale * baseScale.y, + z: 1 + }; + // ---- transform translation (now) ---- + // Tx = -offset.x * scale.x + // => offset.x = -Tx / scale.x + // ---- transform translation (before) ---- + // origin.x = centerX - (centerX - origin.x) * (newZoomX / zoom.x); + // (zoom.x -> initial zoom = base scale, origin.x = 0) + // Tx = origin.x + (trans.x * zoom.x) + var originX = data.scaleCenter.x - data.scaleCenter.x * data.scale; + var originY = data.scaleCenter.y - data.scaleCenter.y * data.scale; + var oldTx = originX + data.translation.x * scale.x; + var oldTy = originY + data.translation.y * scale.y; + offset = { + x: -oldTx / scale.x, + y: -oldTy / scale.y, + z: 0 + }; + } else { + scale = { + x: data.scale.x * baseScale.x, + y: data.scale.y * baseScale.y, + z: 1 + }; + offset = { + x: data.offset.x, + y: data.offset.y, + z: 0 + }; } - // create uri - var uri = 'data:video/' + dataType + ';base64,' + window.btoa(videoDataStr); - return uri; + app.getActiveLayerGroup().setScale(scale); + app.getActiveLayerGroup().setOffset(offset); + // render to draw the view layer + app.render(0); //todo: fix + // drawings (will draw the draw layer) + app.setDrawings(data.drawings, data.drawingsDetails); + }; + /** + * Read an application state from an Object in v0.1 format. + * + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private + */ + function readV01(data) { + // v0.1 -> v0.2 + var v02DAndD = dwv.io.v01Tov02DrawingsAndDetails(data.drawings); + // v0.2 -> v0.3, v0.4 + data.drawings = dwv.io.v02Tov03Drawings(v02DAndD.drawings).toObject(); + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails( + v02DAndD.drawingsDetails); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); + return data; } - /** - * Internal Data URI load. + * Read an application state from an Object in v0.2 format. + * + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private + */ + function readV02(data) { + // v0.2 -> v0.3, v0.4 + data.drawings = dwv.io.v02Tov03Drawings(data.drawings).toObject(); + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails( + dwv.io.v02Tov03DrawingsDetails(data.drawingsDetails)); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); + return data; + } + /** + * Read an application state from an Object in v0.3 format. + * + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private + */ + function readV03(data) { + // v0.3 -> v0.4 + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails(data.drawingsDetails); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); + return data; + } + /** + * Read an application state from an Object in v0.4 format. * - * @param {object} buffer The read data. - * @param {string} origin The data origin. - * @param {number} index The data index. + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private */ - this.load = function (buffer, origin, index) { - // create a DOM video - var video = document.createElement('video'); - if (typeof origin === 'string') { - // url case - var ext = origin.split('.').pop().toLowerCase(); - video.src = createDataUri(buffer, ext); - } else { - video.src = buffer; - } - // storing values to pass them on - video.file = origin; - video.index = index; - // onload handler - video.onloadedmetadata = function (/*event*/) { - try { - dwv.image.getViewFromDOMVideo(this, - self.onloaditem, self.onload, - self.onprogress, self.onloadend, - index, origin); - } catch (error) { - self.onerror({ - error: error, - source: origin - }); - self.onloadend({ - source: origin - }); - } - }; - }; - + function readV04(data) { + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); + return data; + } /** - * Abort load. + * Read an application state from an Object in v0.5 format. + * + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private */ - this.abort = function () { - self.onabort({}); - self.onloadend({}); - }; + function readV05(data) { + return data; + } -}; // class RawVideoLoader +}; // State class /** - * Check if the loader can load the provided file. + * Convert drawings from v0.2 to v0.3. + * v0.2: one layer per slice/frame + * v0.3: one layer, one group per slice. setDrawing expects the full stage * - * @param {object} file The file to check. - * @returns {boolean} True if the file can be loaded. + * @param {Array} drawings An array of drawings. + * @returns {object} The layer with the converted drawings. */ -dwv.io.RawVideoLoader.prototype.canLoadFile = function (file) { - return file.type.match('video.*'); +dwv.io.v02Tov03Drawings = function (drawings) { + // Auxiliar variables + var group, groupShapes, parentGroup; + // Avoid errors when dropping multiple states + //drawLayer.getChildren().each(function(node){ + // node.visible(false); + //}); + + var drawLayer = new Konva.Layer({ + listening: false, + visible: true + }); + + // Get the positions-groups data + var groupDrawings = typeof drawings === 'string' + ? JSON.parse(drawings) : drawings; + // Iterate over each position-groups + for (var k = 0, lenk = groupDrawings.length; k < lenk; ++k) { + // Iterate over each frame + for (var f = 0, lenf = groupDrawings[k].length; f < lenf; ++f) { + groupShapes = groupDrawings[k][f]; + if (groupShapes.length !== 0) { + // Create position-group set as visible and append it to drawLayer + parentGroup = new Konva.Group({ + id: dwv.draw.getDrawPositionGroupId(new dwv.math.Index([1, 1, k, f])), + name: 'position-group', + visible: false + }); + + // Iterate over shapes-group + for (var g = 0, leng = groupShapes.length; g < leng; ++g) { + // create the konva group + group = Konva.Node.create(groupShapes[g]); + // enforce draggable: only the shape was draggable in v0.2, + // now the whole group is. + group.draggable(true); + group.getChildren().forEach(function (gnode) { + gnode.draggable(false); + }); + // add to position group + parentGroup.add(group); + } + // add to layer + drawLayer.add(parentGroup); + } + } + } + + return drawLayer; }; /** - * Check if the loader can load the provided url. + * Convert drawings from v0.1 to v0.2. + * v0.1: text on its own + * v0.2: text as part of label * - * @param {string} url The url to check. - * @returns {boolean} True if the url can be loaded. + * @param {Array} inputDrawings An array of drawings. + * @returns {object} The converted drawings. */ -dwv.io.RawVideoLoader.prototype.canLoadUrl = function (url) { - var urlObjext = dwv.utils.getUrlFromUri(url); - var ext = dwv.utils.getFileExtension(urlObjext.pathname); - return (ext === 'mp4') || (ext === 'ogg') || - (ext === 'webm'); +dwv.io.v01Tov02DrawingsAndDetails = function (inputDrawings) { + var newDrawings = []; + var drawingsDetails = {}; + + var drawGroups; + var drawGroup; + // loop over each slice + for (var k = 0, lenk = inputDrawings.length; k < lenk; ++k) { + // loop over each frame + newDrawings[k] = []; + for (var f = 0, lenf = inputDrawings[k].length; f < lenf; ++f) { + // draw group + drawGroups = inputDrawings[k][f]; + var newFrameDrawings = []; + // Iterate over shapes-group + for (var g = 0, leng = drawGroups.length; g < leng; ++g) { + // create konva group from input + drawGroup = Konva.Node.create(drawGroups[g]); + // force visible (not set in state) + drawGroup.visible(true); + // label position + var pos = {x: 0, y: 0}; + // update shape colour + var kshape = drawGroup.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + kshape.stroke(dwv.utils.colourNameToHex(kshape.stroke())); + // special line case + if (drawGroup.name() === 'line-group') { + // update name + drawGroup.name('ruler-group'); + // add ticks + var ktick0 = new Konva.Line({ + points: [kshape.points()[0], + kshape.points()[1], + kshape.points()[0], + kshape.points()[1]], + name: 'shape-tick0' + }); + drawGroup.add(ktick0); + var ktick1 = new Konva.Line({ + points: [kshape.points()[2], + kshape.points()[3], + kshape.points()[2], + kshape.points()[3]], + name: 'shape-tick1' + }); + drawGroup.add(ktick1); + } + // special protractor case: update arc name + var karcs = drawGroup.getChildren(function (node) { + return node.name() === 'arc'; + }); + if (karcs.length === 1) { + karcs[0].name('shape-arc'); + } + // get its text + var ktexts = drawGroup.getChildren(function (node) { + return node.name() === 'text'; + }); + // update text: move it into a label + var ktext = new Konva.Text({ + name: 'text', + text: '' + }); + if (ktexts.length === 1) { + pos.x = ktexts[0].x(); + pos.y = ktexts[0].y(); + // remove it from the group + ktexts[0].remove(); + // use it + ktext = ktexts[0]; + } else { + // use shape position if no text + if (kshape.points().length !== 0) { + pos = {x: kshape.points()[0], + y: kshape.points()[1]}; + } + } + // create new label with text and tag + var klabel = new Konva.Label({ + x: pos.x, + y: pos.y, + name: 'label' + }); + klabel.add(ktext); + klabel.add(new Konva.Tag()); + // add label to group + drawGroup.add(klabel); + // add group to list + newFrameDrawings.push(JSON.stringify(drawGroup.toObject())); + + // create details (v0.3 format) + var textExpr = ktext.text(); + var txtLen = textExpr.length; + var quant = null; + // adapt to text with flag + if (drawGroup.name() === 'ruler-group') { + quant = { + length: { + value: parseFloat(textExpr.substr(0, txtLen - 2)), + unit: textExpr.substr(-2, 2) + } + }; + textExpr = '{length}'; + } else if (drawGroup.name() === 'ellipse-group' || + drawGroup.name() === 'rectangle-group') { + quant = { + surface: { + value: parseFloat(textExpr.substr(0, txtLen - 3)), + unit: textExpr.substr(-3, 3) + } + }; + textExpr = '{surface}'; + } else if (drawGroup.name() === 'protractor-group' || + drawGroup.name() === 'rectangle-group') { + quant = { + angle: { + value: parseFloat(textExpr.substr(0, txtLen - 1)), + unit: textExpr.substr(-1, 1) + } + }; + textExpr = '{angle}'; + } + // set details + drawingsDetails[drawGroup.id()] = { + textExpr: textExpr, + longText: '', + quant: quant + }; + + } + newDrawings[k].push(newFrameDrawings); + } + } + + return {drawings: newDrawings, drawingsDetails: drawingsDetails}; }; /** - * Get the file content type needed by the loader. + * Convert drawing details from v0.2 to v0.3. + * - v0.2: array [nslices][nframes] with all + * - v0.3: simple array of objects referenced by draw ids * - * @returns {number} One of the 'dwv.io.fileContentTypes'. + * @param {Array} details An array of drawing details. + * @returns {object} The converted drawings. */ -dwv.io.RawVideoLoader.prototype.loadFileAs = function () { - return dwv.io.fileContentTypes.DataURL; +dwv.io.v02Tov03DrawingsDetails = function (details) { + var res = {}; + // Get the positions-groups data + var groupDetails = typeof details === 'string' + ? JSON.parse(details) : details; + // Iterate over each position-groups + for (var k = 0, lenk = groupDetails.length; k < lenk; ++k) { + // Iterate over each frame + for (var f = 0, lenf = groupDetails[k].length; f < lenf; ++f) { + // Iterate over shapes-group + for (var g = 0, leng = groupDetails[k][f].length; g < leng; ++g) { + var group = groupDetails[k][f][g]; + res[group.id] = { + textExpr: group.textExpr, + longText: group.longText, + quant: group.quant + }; + } + } + } + return res; }; /** - * Get the url content type needed by the loader. + * Convert drawing details from v0.3 to v0.4. + * - v0.3: properties at group root + * - v0.4: properties in group meta object * - * @returns {number} One of the 'dwv.io.urlContentTypes'. + * @param {Array} details An array of drawing details. + * @returns {object} The converted drawings. */ -dwv.io.RawVideoLoader.prototype.loadUrlAs = function () { - return dwv.io.urlContentTypes.ArrayBuffer; +dwv.io.v03Tov04DrawingsDetails = function (details) { + var res = {}; + var keys = Object.keys(details); + // Iterate over each position-groups + for (var k = 0, lenk = keys.length; k < lenk; ++k) { + var detail = details[keys[k]]; + res[keys[k]] = { + meta: { + textExpr: detail.textExpr, + longText: detail.longText, + quantification: detail.quant + } + }; + } + return res; }; /** - * Handle a load start event. - * Default does nothing. - * - * @param {object} _event The load start event. - */ -dwv.io.RawVideoLoader.prototype.onloadstart = function (_event) {}; -/** - * Handle a progress event. - * Default does nothing. - * - * @param {object} _event The progress event. - */ -dwv.io.RawVideoLoader.prototype.onprogress = function (_event) {}; -/** - * Handle a load item event. - * Default does nothing. - * - * @param {object} _event The load item event fired - * when a file item has been loaded successfully. - */ -dwv.io.RawVideoLoader.prototype.onloaditem = function (_event) {}; -/** - * Handle a load event. - * Default does nothing. - * - * @param {object} _event The load event fired - * when a file has been loaded successfully. - */ -dwv.io.RawVideoLoader.prototype.onload = function (_event) {}; -/** - * Handle an load end event. - * Default does nothing. - * - * @param {object} _event The load end event fired - * when a file load has completed, successfully or not. - */ -dwv.io.RawVideoLoader.prototype.onloadend = function (_event) {}; -/** - * Handle an error event. - * Default does nothing. - * - * @param {object} _event The error event. - */ -dwv.io.RawVideoLoader.prototype.onerror = function (_event) {}; -/** - * Handle an abort event. - * Default does nothing. + * Convert drawing from v0.4 to v0.5. + * - v0.4: position as object + * - v0.5: position as array * - * @param {object} _event The abort event. + * @param {Array} data An array of drawing. + * @returns {object} The converted drawings. */ -dwv.io.RawVideoLoader.prototype.onabort = function (_event) {}; +dwv.io.v04Tov05Data = function (data) { + var pos = data.position; + data.position = [pos.i, pos.j, pos.k]; + return data; +}; /** - * Add to Loader list. + * Convert drawing from v0.4 to v0.5. + * - v0.4: draw id as 'slice-0_frame-1' + * - v0.5: draw id as '#2-0_#3-1'' + * + * @param {Array} inputDrawings An array of drawing. + * @returns {object} The converted drawings. */ -dwv.io.loaderList = dwv.io.loaderList || []; -dwv.io.loaderList.push('RawVideoLoader'); +dwv.io.v04Tov05Drawings = function (inputDrawings) { + // Iterate over each position-groups + var posGroups = inputDrawings.children; + for (var k = 0, lenk = posGroups.length; k < lenk; ++k) { + var posGroup = posGroups[k]; + var id = posGroup.attrs.id; + var ids = id.split('_'); + var sliceNumber = parseInt(ids[0].substring(6), 10); // 'slice-0' + var frameNumber = parseInt(ids[1].substring(6), 10); // 'frame-0' + var newId = '#2-'; + if (sliceNumber === 0 && frameNumber !== 0) { + newId += frameNumber; + } else { + newId += sliceNumber; + } + posGroup.attrs.id = newId; + } + return inputDrawings; +}; // namespaces var dwv = dwv || {}; @@ -19068,6 +21715,7 @@ dwv.io.UrlsLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: self.getDefaultCharacterSet() }); // set loader callbacks @@ -19632,6 +22280,7 @@ dwv.math.BucketQueue.prototype.pop = function () { return ret; }; +// TODO: needs at least two items... dwv.math.BucketQueue.prototype.remove = function (item) { // Tries to remove item from queue. Returns true on success, false otherwise if (!item) { @@ -19642,7 +22291,10 @@ dwv.math.BucketQueue.prototype.remove = function (item) { var bucket = this.getBucket(item); var node = this.buckets[bucket]; - while (node !== null && !item.equals(node.next)) { + while (node !== null && + !(node.next !== null && + item.x === node.next.x && + item.y === node.next.y)) { node = node.next; } @@ -19703,14 +22355,15 @@ dwv.math.mulABC = function (a, b, c) { * Circle shape. * * @class - * @param {object} centre A Point2D representing the centre of the circle. + * @param {dwv.math.Point2D} centre A Point2D representing the centre + * of the circle. * @param {number} radius The radius of the circle. */ dwv.math.Circle = function (centre, radius) { /** * Get the centre (point) of the circle. * - * @returns {object} The center (point) of the circle. + * @returns {dwv.math.Point2D} The center (point) of the circle. */ this.getCenter = function () { return centre; @@ -19730,7 +22383,7 @@ dwv.math.Circle = function (centre, radius) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Circle} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Circle.prototype.equals = function (rhs) { @@ -19800,7 +22453,8 @@ dwv.math.Circle.prototype.getRound = function () { /** * Quantify an circle according to view information. * - * @param {object} viewController The associated view controller. + * @param {dwv.ctrl.ViewController} viewController The associated view + * controller. * @param {Array} flags A list of stat values to calculate. * @returns {object} A quantification object. */ @@ -19864,7 +22518,8 @@ dwv.math.mulABC = function (a, b, c) { * Ellipse shape. * * @class - * @param {object} centre A Point2D representing the centre of the ellipse. + * @param {dwv.math.Point2D} centre A Point2D representing the centre + * of the ellipse. * @param {number} a The radius of the ellipse on the horizontal axe. * @param {number} b The radius of the ellipse on the vertical axe. */ @@ -19872,7 +22527,7 @@ dwv.math.Ellipse = function (centre, a, b) { /** * Get the centre (point) of the ellipse. * - * @returns {object} The center (point) of the ellipse. + * @returns {dwv.math.Point2D} The center (point) of the ellipse. */ this.getCenter = function () { return centre; @@ -19900,7 +22555,7 @@ dwv.math.Ellipse = function (centre, a, b) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Ellipse} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Ellipse.prototype.equals = function (rhs) { @@ -19957,59 +22612,274 @@ dwv.math.Ellipse.prototype.getRound = function () { if (Math.abs(diff) < 1e-7) { continue; } - var transX = radiusRatio * Math.sqrt(diff); - // remove small values - if (transX < 0.5) { - continue; + var transX = radiusRatio * Math.sqrt(diff); + // remove small values + if (transX < 0.5) { + continue; + } + regions.push([ + [Math.round(centerX - transX), Math.round(y)], + [Math.round(centerX + transX), Math.round(y)] + ]); + } + return regions; +}; + +/** + * Quantify an ellipse according to view information. + * + * @param {dwv.ctrl.ViewController} viewController The associated view + * controller. + * @param {Array} flags A list of stat values to calculate. + * @returns {object} A quantification object. + */ +dwv.math.Ellipse.prototype.quantify = function (viewController, flags) { + var quant = {}; + // surface + var spacing = viewController.get2DSpacing(); + var surface = this.getWorldSurface(spacing[0], spacing[1]); + if (surface !== null) { + quant.surface = {value: surface / 100, unit: dwv.i18n('unit.cm2')}; + } + + // pixel quantification + if (viewController.canQuantifyImage()) { + var regions = this.getRound(); + if (regions.length !== 0) { + var values = viewController.getImageVariableRegionValues(regions); + var quantif = dwv.math.getStats(values, flags); + quant.min = {value: quantif.getMin(), unit: ''}; + quant.max = {value: quantif.getMax(), unit: ''}; + quant.mean = {value: quantif.getMean(), unit: ''}; + quant.stdDev = {value: quantif.getStdDev(), unit: ''}; + if (typeof quantif.getMedian !== 'undefined') { + quant.median = {value: quantif.getMedian(), unit: ''}; + } + if (typeof quantif.getP25 !== 'undefined') { + quant.p25 = {value: quantif.getP25(), unit: ''}; + } + if (typeof quantif.getP75 !== 'undefined') { + quant.p75 = {value: quantif.getP75(), unit: ''}; + } + } + } + + // return + return quant; +}; + +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Immutable index. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The index values. + */ +dwv.math.Index = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create index with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create index with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val); + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create index with non number values.'); + } + + /** + * Get the index value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the index. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the Index. + * + * @returns {string} The Index as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this index. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Index class + +/** + * Check if the input index can be compared to this one. + * + * @param {dwv.math.Index} rhs The index to compare to. + * @returns {boolean} True if both indices are comparable. + */ +dwv.math.Index.prototype.canCompare = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + if (this.length() !== rhs.length()) { + return false; + } + // seems ok! + return true; +}; + +/** + * Check for Index equality. + * + * @param {dwv.math.Index} rhs The index to compare to. + * @returns {boolean} True if both indices are equal. + */ +dwv.math.Index.prototype.equals = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return false; + } + // check values + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Add another index to this one. + * + * @param {dwv.math.Index} rhs The index to add. + * @returns {dwv.math.Index} The index representing the sum of both indices. + */ +dwv.math.Index.prototype.add = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + // add values + var values = []; + for (var i = 0, leni = this.length(); i < leni; ++i) { + values.push(this.get(i) + rhs.get(i)); + } + // seems ok! + return new dwv.math.Index(values); +}; + +/** + * Get the current index with a new 2D base. + * + * @param {number} i The new 0 index. + * @param {number} j The new 1 index. + * @returns {dwv.math.Index} The new index. + */ +dwv.math.Index.prototype.getWithNew2D = function (i, j) { + var values = [i, j]; + for (var l = 2, lenl = this.length(); l < lenl; ++l) { + values.push(this.get(l)); + } + return new dwv.math.Index(values); +}; + +/** + * Get an index with values set to 0 and the input size. + * + * @param {number} size The size of the index. + * @returns {dwv.math.Index} The zero index. + */ +dwv.math.getZeroIndex = function (size) { + var values = new Array(size); + values.fill(0); + return new dwv.math.Index(values); +}; + +/** + * Get a string id from the index values in the form of: '#0-1_#1-2'. + * + * @param {Array} dims Optional list of dimensions to use. + * @returns {string} The string id. + */ +dwv.math.Index.prototype.toStringId = function (dims) { + if (typeof dims === 'undefined') { + dims = []; + for (var j = 0; j < this.length(); ++j) { + dims.push(j); + } + } + for (var ii = 0; ii < dims.length; ++ii) { + if (dims[ii] >= this.length()) { + throw new Error('Non valid dimension for toStringId.'); + } + } + var res = ''; + for (var i = 0; i < dims.length; ++i) { + if (i !== 0) { + res += '_'; } - regions.push([ - [Math.round(centerX - transX), Math.round(y)], - [Math.round(centerX + transX), Math.round(y)] - ]); + res += '#' + dims[i] + '-' + this.get(dims[i]); } - return regions; + return res; }; /** - * Quantify an ellipse according to view information. + * Get an index from an id string in the form of: '#0-1_#1-2' + * (result of index.toStringId). * - * @param {object} viewController The associated view controller. - * @param {Array} flags A list of stat values to calculate. - * @returns {object} A quantification object. + * @param {string} inputStr The input string. + * @returns {dwv.math.Index} The corresponding index. */ -dwv.math.Ellipse.prototype.quantify = function (viewController, flags) { - var quant = {}; - // surface - var spacing = viewController.get2DSpacing(); - var surface = this.getWorldSurface(spacing[0], spacing[1]); - if (surface !== null) { - quant.surface = {value: surface / 100, unit: dwv.i18n('unit.cm2')}; - } - - // pixel quantification - if (viewController.canQuantifyImage()) { - var regions = this.getRound(); - if (regions.length !== 0) { - var values = viewController.getImageVariableRegionValues(regions); - var quantif = dwv.math.getStats(values, flags); - quant.min = {value: quantif.getMin(), unit: ''}; - quant.max = {value: quantif.getMax(), unit: ''}; - quant.mean = {value: quantif.getMean(), unit: ''}; - quant.stdDev = {value: quantif.getStdDev(), unit: ''}; - if (typeof quantif.getMedian !== 'undefined') { - quant.median = {value: quantif.getMedian(), unit: ''}; - } - if (typeof quantif.getP25 !== 'undefined') { - quant.p25 = {value: quantif.getP25(), unit: ''}; - } - if (typeof quantif.getP75 !== 'undefined') { - quant.p75 = {value: quantif.getP75(), unit: ''}; - } +dwv.math.getIndexFromStringId = function (inputStr) { + // split ids + var strIds = inputStr.split('_'); + // get the size of the index + var pointLength = 0; + var dim; + for (var i = 0; i < strIds.length; ++i) { + dim = parseInt(strIds[i].substring(1, 2), 10); + if (dim > pointLength) { + pointLength = dim; } } - - // return - return quant; + if (pointLength === 0) { + throw new Error('No dimension found in point stringId'); + } + // default values + var values = new Array(pointLength); + values.fill(0); + // get other values from the input string + for (var j = 0; j < strIds.length; ++j) { + dim = parseInt(strIds[j].substring(1, 3), 10); + var value = parseInt(strIds[j].substring(3), 10); + values[dim] = value; + } + return new dwv.math.Point(values); }; // namespaces @@ -20020,14 +22890,15 @@ dwv.math = dwv.math || {}; * Line shape. * * @class - * @param {object} begin A Point2D representing the beginning of the line. - * @param {object} end A Point2D representing the end of the line. + * @param {dwv.math.Point2D} begin A Point2D representing the beginning + * of the line. + * @param {dwv.math.Point2D} end A Point2D representing the end of the line. */ dwv.math.Line = function (begin, end) { /** * Get the begin point of the line. * - * @returns {object} The beginning point of the line. + * @returns {dwv.math.Point2D} The beginning point of the line. */ this.getBegin = function () { return begin; @@ -20036,7 +22907,7 @@ dwv.math.Line = function (begin, end) { /** * Get the end point of the line. * - * @returns {object} The ending point of the line. + * @returns {dwv.math.Point2D} The ending point of the line. */ this.getEnd = function () { return end; @@ -20046,7 +22917,7 @@ dwv.math.Line = function (begin, end) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Line} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Line.prototype.equals = function (rhs) { @@ -20106,7 +22977,7 @@ dwv.math.Line.prototype.getWorldLength = function (spacingX, spacingY) { /** * Get the mid point of the line. * - * @returns {object} The mid point of the line. + * @returns {dwv.math.Point2D} The mid point of the line. */ dwv.math.Line.prototype.getMidpoint = function () { return new dwv.math.Point2D( @@ -20151,8 +23022,8 @@ dwv.math.Line.prototype.getInclination = function () { /** * Get the angle between two lines in degree. * - * @param {object} line0 The first line. - * @param {object} line1 The second line. + * @param {dwv.math.Line} line0 The first line. + * @param {dwv.math.Line} line1 The second line. * @returns {number} The angle. */ dwv.math.getAngle = function (line0, line1) { @@ -20174,8 +23045,8 @@ dwv.math.getAngle = function (line0, line1) { /** * Get a perpendicular line to an input one. * - * @param {object} line The line to be perpendicular to. - * @param {object} point The middle point of the perpendicular line. + * @param {dwv.math.Line} line The line to be perpendicular to. + * @param {dwv.math.Point2D} point The middle point of the perpendicular line. * @param {number} length The length of the perpendicular line. * @returns {object} A perpendicular line. */ @@ -20249,34 +23120,35 @@ var dwv = dwv || {}; dwv.math = dwv.math || {}; // difference between 1 and the smallest floating point number greater than 1 +// -> ~2e-16 if (typeof Number.EPSILON === 'undefined') { Number.EPSILON = Math.pow(2, -52); } +// -> ~2e-12 +dwv.math.BIG_EPSILON = Number.EPSILON * 1e4; + +/** + * Check if two numbers are similar. + * + * @param {number} a The first number. + * @param {number} b The second number. + * @param {number} tol The comparison tolerance. + * @returns {boolean} True if similar. + */ +dwv.math.isSimilar = function (a, b, tol) { + if (typeof tol === 'undefined') { + tol = Number.EPSILON; + } + return Math.abs(a - b) < tol; +}; /** * Immutable 3x3 Matrix. * - * @param {number} m00 m[0][0] - * @param {number} m01 m[0][1] - * @param {number} m02 m[0][2] - * @param {number} m10 m[1][0] - * @param {number} m11 m[1][1] - * @param {number} m12 m[1][2] - * @param {number} m20 m[2][0] - * @param {number} m21 m[2][1] - * @param {number} m22 m[2][2] + * @param {Array} values row-major ordered 9 values. * @class */ -dwv.math.Matrix33 = function ( - m00, m01, m02, - m10, m11, m12, - m20, m21, m22) { - // row-major order - var mat = new Float32Array(9); - mat[0] = m00; mat[1] = m01; mat[2] = m02; - mat[3] = m10; mat[4] = m11; mat[5] = m12; - mat[6] = m20; mat[7] = m21; mat[8] = m22; - +dwv.math.Matrix33 = function (values) { /** * Get a value of the matrix. * @@ -20285,32 +23157,28 @@ dwv.math.Matrix33 = function ( * @returns {number} The value at the position. */ this.get = function (row, col) { - return mat[row * 3 + col]; + return values[row * 3 + col]; }; }; // Matrix33 /** * Check for Matrix33 equality. * - * @param {object} rhs The other matrix to compare to. + * @param {dwv.math.Matrix33} rhs The other matrix to compare to. * @param {number} p A numeric expression for the precision to use in check * (ex: 0.001). Defaults to Number.EPSILON if not provided. * @returns {boolean} True if both matrices are equal. */ dwv.math.Matrix33.prototype.equals = function (rhs, p) { - if (typeof p === 'undefined') { - p = Number.EPSILON; - } - - return Math.abs(this.get(0, 0) - rhs.get(0, 0)) < p && - Math.abs(this.get(0, 1) - rhs.get(0, 1)) < p && - Math.abs(this.get(0, 2) - rhs.get(0, 2)) < p && - Math.abs(this.get(1, 0) - rhs.get(1, 0)) < p && - Math.abs(this.get(1, 1) - rhs.get(1, 1)) < p && - Math.abs(this.get(1, 2) - rhs.get(1, 2)) < p && - Math.abs(this.get(2, 0) - rhs.get(2, 0)) < p && - Math.abs(this.get(2, 1) - rhs.get(2, 1)) < p && - Math.abs(this.get(2, 2) - rhs.get(2, 2)) < p; + return dwv.math.isSimilar(this.get(0, 0), rhs.get(0, 0), p) && + dwv.math.isSimilar(this.get(0, 1), rhs.get(0, 1), p) && + dwv.math.isSimilar(this.get(0, 2), rhs.get(0, 2), p) && + dwv.math.isSimilar(this.get(1, 0), rhs.get(1, 0), p) && + dwv.math.isSimilar(this.get(1, 1), rhs.get(1, 1), p) && + dwv.math.isSimilar(this.get(1, 2), rhs.get(1, 2), p) && + dwv.math.isSimilar(this.get(2, 0), rhs.get(2, 0), p) && + dwv.math.isSimilar(this.get(2, 1), rhs.get(2, 1), p) && + dwv.math.isSimilar(this.get(2, 2), rhs.get(2, 2), p); }; /** @@ -20325,38 +23193,310 @@ dwv.math.Matrix33.prototype.toString = function () { ']'; }; +/** + * Multiply this matrix by another. + * + * @param {dwv.math.Matrix33} rhs The matrix to multiply by. + * @returns {dwv.math.Matrix33} The product matrix. + */ +dwv.math.Matrix33.prototype.multiply = function (rhs) { + var values = []; + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 3; ++j) { + var tmp = 0; + for (var k = 0; k < 3; ++k) { + tmp += this.get(i, k) * rhs.get(k, j); + } + values.push(tmp); + } + } + return new dwv.math.Matrix33(values); +}; + +/** + * Get the absolute value of this matrix. + * + * @returns {dwv.math.Matrix33} The result matrix. + */ +dwv.math.Matrix33.prototype.getAbs = function () { + var values = []; + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 3; ++j) { + values.push(Math.abs(this.get(i, j))); + } + } + return new dwv.math.Matrix33(values); +}; + +/** + * Multiply this matrix by a 3D array. + * + * @param {Array} array3D The input 3D array. + * @returns {Array} The result 3D array. + */ +dwv.math.Matrix33.prototype.multiplyArray3D = function (array3D) { + if (array3D.length !== 3) { + throw new Error('Cannot multiply 3x3 matrix with non 3D array: ', + array3D.length); + } + var values = []; + for (var i = 0; i < 3; ++i) { + var tmp = 0; + for (var j = 0; j < 3; ++j) { + tmp += this.get(i, j) * array3D[j]; + } + values.push(tmp); + } + return values; +}; + /** * Multiply this matrix by a 3D vector. * - * @param {object} vector3D The input 3D vector - * @returns {object} The result 3D vector - */ -dwv.math.Matrix33.multiplyVector3D = function (vector3D) { - // cache matrix values - var m00 = this.get(0, 0); var m01 = this.get(0, 1); var m02 = this.get(0, 2); - var m10 = this.get(1, 0); var m11 = this.get(1, 1); var m12 = this.get(1, 2); - var m20 = this.get(2, 0); var m21 = this.get(2, 1); var m22 = this.get(2, 2); - // cache vector values - var vx = vector3D.getX(); - var vy = vector3D.getY(); - var vz = vector3D.getZ(); - // calculate - return new dwv.math.Vector3D( - (m00 * vx) + (m01 * vy) + (m02 * vz), - (m10 * vx) + (m11 * vy) + (m12 * vz), - (m20 * vx) + (m21 * vy) + (m22 * vz)); + * @param {dwv.math.Vector3D} vector3D The input 3D vector. + * @returns {dwv.math.Vector3D} The result 3D vector. + */ +dwv.math.Matrix33.prototype.multiplyVector3D = function (vector3D) { + var array3D = this.multiplyArray3D( + [vector3D.getX(), vector3D.getY(), vector3D.getZ()] + ); + return new dwv.math.Vector3D(array3D[0], array3D[1], array3D[2]); +}; + +/** + * Multiply this matrix by a 3D point. + * + * @param {dwv.math.Point3D} point3D The input 3D point. + * @returns {dwv.math.Point3D} The result 3D point. + */ +dwv.math.Matrix33.prototype.multiplyPoint3D = function (point3D) { + var array3D = this.multiplyArray3D( + [point3D.getX(), point3D.getY(), point3D.getZ()] + ); + return new dwv.math.Point3D(array3D[0], array3D[1], array3D[2]); +}; + +/** + * Multiply this matrix by a 3D index. + * + * @param {dwv.math.Index} index3D The input 3D index. + * @returns {dwv.math.Index} The result 3D index. + */ +dwv.math.Matrix33.prototype.multiplyIndex3D = function (index3D) { + var array3D = this.multiplyArray3D(index3D.getValues()); + return new dwv.math.Index(array3D); +}; + +/** + * Get the inverse of this matrix. + * + * @returns {dwv.math.Matrix33} The inverse matrix. + * @see https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_3_%C3%97_3_matrices + */ +dwv.math.Matrix33.prototype.getInverse = function () { + var a = this.get(0, 0); + var b = this.get(0, 1); + var c = this.get(0, 2); + var d = this.get(1, 0); + var e = this.get(1, 1); + var f = this.get(1, 2); + var g = this.get(2, 0); + var h = this.get(2, 1); + var i = this.get(2, 2); + + var a2 = e * i - f * h; + var b2 = f * g - d * i; + var c2 = d * h - e * g; + + var det = a * a2 + b * b2 + c * c2; + if (det === 0) { + dwv.logger.warn('Cannot invert matrix with zero determinant.'); + return; + } + + var values = [ + a2 / det, + (c * h - b * i) / det, + (b * f - c * e) / det, + b2 / det, + (a * i - c * g) / det, + (c * d - a * f) / det, + c2 / det, + (b * g - a * h) / det, + (a * e - b * d) / det + ]; + + return new dwv.math.Matrix33(values); +}; + +/** + * Get the index of the maximum in absolute value of a row. + * + * @param {number} row The row to get the maximum from. + * @returns {object} The {value,index} of the maximum. + */ +dwv.math.Matrix33.prototype.getRowAbsMax = function (row) { + var values = [ + Math.abs(this.get(row, 0)), + Math.abs(this.get(row, 1)), + Math.abs(this.get(row, 2)) + ]; + var absMax = Math.max.apply(null, values); + var index = values.indexOf(absMax); + return { + value: this.get(row, index), + index: index + }; +}; + +/** + * Get the index of the maximum in absolute value of a column. + * + * @param {number} col The column to get the maximum from. + * @returns {object} The {value,index} of the maximum. + */ +dwv.math.Matrix33.prototype.getColAbsMax = function (col) { + var values = [ + Math.abs(this.get(0, col)), + Math.abs(this.get(1, col)), + Math.abs(this.get(2, col)) + ]; + var absMax = Math.max.apply(null, values); + var index = values.indexOf(absMax); + return { + value: this.get(index, col), + index: index + }; +}; + +/** + * Get this matrix with only zero and +/- ones instead of the maximum, + * + * @returns {dwv.math.Matrix33} The simplified matrix. + */ +dwv.math.Matrix33.prototype.asOneAndZeros = function () { + var res = []; + for (var j = 0; j < 3; ++j) { + var max = this.getRowAbsMax(j); + var sign = max.value > 0 ? 1 : -1; + for (var i = 0; i < 3; ++i) { + if (i === max.index) { + //res.push(1); + res.push(1 * sign); + } else { + res.push(0); + } + } + } + return new dwv.math.Matrix33(res); +}; + +/** + * Get the third column direction index of an orientation matrix. + * + * @returns {number} The index of the absolute maximum of the last column. + */ +dwv.math.Matrix33.prototype.getThirdColMajorDirection = function () { + return this.getColAbsMax(2).index; }; /** * Create a 3x3 identity matrix. * - * @returns {object} The identity matrix. + * @returns {dwv.math.Matrix33} The identity matrix. */ dwv.math.getIdentityMat33 = function () { - return new dwv.math.Matrix33( + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ 1, 0, 0, 0, 1, 0, - 0, 0, 1); + 0, 0, 1 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Check if a matrix is a 3x3 identity matrix. + * + * @param {dwv.math.Matrix33} mat33 The matrix to test. + * @returns {boolean} True if identity. + */ +dwv.math.isIdentityMat33 = function (mat33) { + return mat33.equals(dwv.math.getIdentityMat33()); +}; + +/** + * Create a 3x3 coronal (xzy) matrix. + * + * @returns {dwv.math.Matrix33} The coronal matrix. + */ +dwv.math.getCoronalMat33 = function () { + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ + 1, 0, 0, + 0, 0, 1, + 0, 1, 0 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Create a 3x3 sagittal (yzx) matrix. + * + * @returns {dwv.math.Matrix33} The sagittal matrix. + */ +dwv.math.getSagittalMat33 = function () { + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ + 0, 0, 1, + 1, 0, 0, + 0, 1, 0 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Get an orientation matrix from a name. + * + * @param {string} name The orientation name. + * @returns {dwv.math.Matrix33} The orientation matrix. + */ +dwv.math.getMatrixFromName = function (name) { + var matrix = null; + if (name === 'axial') { + matrix = dwv.math.getIdentityMat33(); + } else if (name === 'coronal') { + matrix = dwv.math.getCoronalMat33(); + } else if (name === 'sagittal') { + matrix = dwv.math.getSagittalMat33(); + } + return matrix; +}; + +/** + * Get the oriented values of an input 3D array. + * + * @param {Array} array3D The 3D array. + * @param {dwv.math.Matrix33} orientation The orientation 3D matrix. + * @returns {Array} The values reordered according to the orientation. + */ +dwv.math.getOrientedArray3D = function (array3D, orientation) { + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + return orientation.getInverse().getAbs().multiplyArray3D(array3D); +}; + +/** + * Get the raw values of an oriented input 3D array. + * + * @param {Array} array3D The 3D array. + * @param {dwv.math.Matrix33} orientation The orientation 3D matrix. + * @returns {Array} The values reordered to compensate the orientation. + */ +dwv.math.getDeOrientedArray3D = function (array3D, orientation) { + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + return orientation.getAbs().multiplyArray3D(array3D); }; // namespaces @@ -20393,7 +23533,7 @@ dwv.math.Path = function (inputPointArray, inputControlPointIndexArray) { * Get a point of the list. * * @param {number} index The index of the point to get (beware, no size check). - * @returns {object} The Point2D at the given index. + * @returns {dwv.math.Point2D} The Point2D at the given index. */ dwv.math.Path.prototype.getPoint = function (index) { return this.pointArray[index]; @@ -20402,7 +23542,7 @@ dwv.math.Path.prototype.getPoint = function (index) { /** * Is the given point a control point. * - * @param {object} point The Point2D to check. + * @param {dwv.math.Point2D} point The Point2D to check. * @returns {boolean} True if a control point. */ dwv.math.Path.prototype.isControlPoint = function (point) { @@ -20426,7 +23566,7 @@ dwv.math.Path.prototype.getLength = function () { /** * Add a point to the path. * - * @param {object} point The Point2D to add. + * @param {dwv.math.Point2D} point The Point2D to add. */ dwv.math.Path.prototype.addPoint = function (point) { this.pointArray.push(point); @@ -20435,7 +23575,7 @@ dwv.math.Path.prototype.addPoint = function (point) { /** * Add a control point to the path. * - * @param {object} point The Point2D to make a control point. + * @param {dwv.math.Point2D} point The Point2D to make a control point. */ dwv.math.Path.prototype.addControlPoint = function (point) { var index = this.pointArray.indexOf(point); @@ -20443,7 +23583,7 @@ dwv.math.Path.prototype.addControlPoint = function (point) { this.controlPointIndexArray.push(index); } else { throw new Error( - 'Error: addControlPoint called with no point in list point.'); + 'Cannot mark a non registered point as control point.'); } }; @@ -20504,7 +23644,7 @@ dwv.math.Point2D = function (x, y) { /** * Check for Point2D equality. * - * @param {object} rhs The other point to compare to. + * @param {dwv.math.Point2D} rhs The other point to compare to. * @returns {boolean} True if both points are equal. */ dwv.math.Point2D.prototype.equals = function (rhs) { @@ -20546,39 +23686,6 @@ dwv.math.Point2D.prototype.getRound = function () { ); }; -/** - * Mutable 2D point. - * - * @class - * @param {number} x The X coordinate for the point. - * @param {number} y The Y coordinate for the point. - */ -dwv.math.FastPoint2D = function (x, y) { - this.x = x; - this.y = y; -}; // FastPoint2D class - -/** - * Check for FastPoint2D equality. - * - * @param {object} rhs The other point to compare to. - * @returns {boolean} True if both points are equal. - */ -dwv.math.FastPoint2D.prototype.equals = function (rhs) { - return rhs !== null && - this.x === rhs.x && - this.y === rhs.y; -}; - -/** - * Get a string representation of the FastPoint2D. - * - * @returns {string} The point as a string. - */ -dwv.math.FastPoint2D.prototype.toString = function () { - return '(' + this.x + ', ' + this.y + ')'; -}; - /** * Immutable 3D point. * @@ -20617,7 +23724,7 @@ dwv.math.Point3D = function (x, y, z) { /** * Check for Point3D equality. * - * @param {object} rhs The other point to compare to. + * @param {dwv.math.Point3D} rhs The other point to compare to. * @returns {boolean} True if both points are equal. */ dwv.math.Point3D.prototype.equals = function (rhs) { @@ -20655,7 +23762,7 @@ dwv.math.Point3D.prototype.getDistance = function (point3D) { * Get the difference to another Point3D. * * @param {dwv.math.Point3D} point3D The input point. - * @returns {object} The 3D vector from the input point to this one. + * @returns {dwv.math.Point3D} The 3D vector from the input point to this one. */ dwv.math.Point3D.prototype.minus = function (point3D) { return new dwv.math.Vector3D( @@ -20665,62 +23772,168 @@ dwv.math.Point3D.prototype.minus = function (point3D) { }; /** - * Immutable 3D index. + * Immutable point. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The point values. + */ +dwv.math.Point = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create point with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create point with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val); + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create point with non number values.'); + } + + /** + * Get the index value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the index. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the Index. + * + * @returns {string} The Index as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this index. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Point class + +/** + * Check if the input point can be compared to this one. + * + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {boolean} True if both points are comparable. + */ +dwv.math.Point.prototype.canCompare = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + if (this.length() !== rhs.length()) { + return false; + } + // seems ok! + return true; +}; + +/** + * Check for Point equality. + * + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {boolean} True if both points are equal. + */ +dwv.math.Point.prototype.equals = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return false; + } + // check values + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Compare points and return different dimensions. + * + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {Array} The list of different dimensions. + */ +dwv.math.Point.prototype.compare = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + // check values + var diffDims = []; + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + diffDims.push(i); + } + } + return diffDims; +}; + +/** + * Get the 3D part of this point. * - * @class - * @param {number} i The column index. - * @param {number} j The row index. - * @param {number} k The slice index. + * @returns {dwv.math.Point3D} The Point3D. */ -dwv.math.Index3D = function (i, j, k) { - /** - * Get the column index. - * - * @returns {number} The column index. - */ - this.getI = function () { - return i; - }; - /** - * Get the row index. - * - * @returns {number} The row index. - */ - this.getJ = function () { - return j; - }; - /** - * Get the slice index. - * - * @returns {number} The slice index. - */ - this.getK = function () { - return k; - }; -}; // Index3D class +dwv.math.Point.prototype.get3D = function () { + return new dwv.math.Point3D(this.get(0), this.get(1), this.get(2)); +}; /** - * Check for Index3D equality. + * Add another point to this one. * - * @param {object} rhs The other index to compare to. - * @returns {boolean} True if both indices are equal. + * @param {dwv.math.Point} rhs The point to add. + * @returns {dwv.math.Point} The point representing the sum of both points. */ -dwv.math.Index3D.prototype.equals = function (rhs) { - return rhs !== null && - this.getI() === rhs.getI() && - this.getJ() === rhs.getJ() && - this.getK() === rhs.getK(); +dwv.math.Point.prototype.add = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + var values = []; + var values0 = this.getValues(); + var values1 = rhs.getValues(); + for (var i = 0; i < values0.length; ++i) { + values.push(values0[i] + values1[i]); + } + return new dwv.math.Point(values); }; /** - * Get a string representation of the Index3D. + * Merge this point with a Point3D to create a new point. * - * @returns {string} The Index3D as a string. + * @param {dwv.math.Point3D} rhs The Point3D to merge with. + * @returns {dwv.math.Point} The merge result. */ -dwv.math.Index3D.prototype.toString = function () { - return '(' + this.getI() + - ', ' + this.getJ() + - ', ' + this.getK() + ')'; +dwv.math.Point.prototype.mergeWith3D = function (rhs) { + var values = this.getValues(); + values[0] = rhs.getX(); + values[1] = rhs.getY(); + values[2] = rhs.getZ(); + return new dwv.math.Point(values); }; // namespaces @@ -20748,8 +23961,10 @@ dwv.math.mulABC = function (a, b, c) { * Rectangle shape. * * @class - * @param {object} begin A Point2D representing the beginning of the rectangle. - * @param {object} end A Point2D representing the end of the rectangle. + * @param {dwv.math.Point2D} begin A Point2D representing the beginning + * of the rectangle. + * @param {dwv.math.Point2D} end A Point2D representing the end + * of the rectangle. */ dwv.math.Rectangle = function (begin, end) { if (end.getX() < begin.getX()) { @@ -20766,7 +23981,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Get the begin point of the rectangle. * - * @returns {object} The begin point of the rectangle + * @returns {dwv.math.Point2D} The begin point of the rectangle */ this.getBegin = function () { return begin; @@ -20775,7 +23990,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Get the end point of the rectangle. * - * @returns {object} The end point of the rectangle + * @returns {dwv.math.Point2D} The end point of the rectangle */ this.getEnd = function () { return end; @@ -20785,7 +24000,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Rectangle} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Rectangle.prototype.equals = function (rhs) { @@ -20930,7 +24145,7 @@ dwv.math.ROI = function () { * * @param {number} index The index of the point to get * (beware, no size check). - * @returns {object} The Point2D at the given index. + * @returns {dwv.math.Point2D} The Point2D at the given index. */ this.getPoint = function (index) { return points[index]; @@ -20946,7 +24161,7 @@ dwv.math.ROI = function () { /** * Add a point to the ROI. * - * @param {object} point The Point2D to add. + * @param {dwv.math.Point2D} point The Point2D to add. */ this.addPoint = function (point) { points.push(point); @@ -21165,8 +24380,8 @@ dwv.math.gradUnitVector = function (gradX, gradY, px, py, out) { }; dwv.math.gradDirection = function (gradX, gradY, px, py, qx, qy) { - var __dgpuv = new dwv.math.FastPoint2D(-1, -1); - var __gdquv = new dwv.math.FastPoint2D(-1, -1); + var __dgpuv = {x: -1, y: -1}; + var __gdquv = {x: -1, y: -1}; // Compute the gradiant direction, in radians, between to points dwv.math.gradUnitVector(gradX, gradY, px, py, __dgpuv); dwv.math.gradUnitVector(gradX, gradY, qx, qy, __gdquv); @@ -21199,7 +24414,7 @@ dwv.math.computeSides = function (dist, gradX, gradY, greyscale) { sides.inside = []; sides.outside = []; - var guv = new dwv.math.FastPoint2D(-1, -1); // Current gradient unit vector + var guv = {x: -1, y: -1}; // Current gradient unit vector for (var y = 0; y < gradX.length; y++) { sides.inside[y] = []; @@ -21483,7 +24698,7 @@ dwv.math.Scissors.prototype.adj = function (p) { for (var y = sy; y <= ey; y++) { for (var x = sx; x <= ex; x++) { if (x !== p.x || y !== p.y) { - list[idx++] = new dwv.math.FastPoint2D(x, y); + list[idx++] = {x: x, y: y}; } } } @@ -21865,9 +25080,9 @@ dwv.math.Vector3D = function (x, y, z) { */ dwv.math.Vector3D.prototype.equals = function (rhs) { return rhs !== null && - this.getX() === rhs.getX() && - this.getY() === rhs.getY() && - this.getZ() === rhs.getZ(); + this.getX() === rhs.getX() && + this.getY() === rhs.getY() && + this.getZ() === rhs.getZ(); }; /** @@ -21877,8 +25092,8 @@ dwv.math.Vector3D.prototype.equals = function (rhs) { */ dwv.math.Vector3D.prototype.toString = function () { return '(' + this.getX() + - ', ' + this.getY() + - ', ' + this.getZ() + ')'; + ', ' + this.getY() + + ', ' + this.getZ() + ')'; }; /** @@ -21887,9 +25102,11 @@ dwv.math.Vector3D.prototype.toString = function () { * @returns {number} The norm. */ dwv.math.Vector3D.prototype.norm = function () { - return Math.sqrt((this.getX() * this.getX()) + - (this.getY() * this.getY()) + - (this.getZ() * this.getZ())); + return Math.sqrt( + (this.getX() * this.getX()) + + (this.getY() * this.getY()) + + (this.getZ() * this.getZ()) + ); }; /** @@ -21897,8 +25114,9 @@ dwv.math.Vector3D.prototype.norm = function () { * vector that is perpendicular to both a and b. * If both vectors are parallel, the cross product is a zero vector. * - * @param {object} vector3D The input vector. - * @returns {object} The result vector. + * @see https://en.wikipedia.org/wiki/Cross_product + * @param {dwv.math.Vector3D} vector3D The input vector. + * @returns {dwv.math.Vector3D} The result vector. */ dwv.math.Vector3D.prototype.crossProduct = function (vector3D) { return new dwv.math.Vector3D( @@ -21910,13 +25128,14 @@ dwv.math.Vector3D.prototype.crossProduct = function (vector3D) { /** * Get the dot product with another Vector3D. * - * @param {object} vector3D The input vector. + * @see https://en.wikipedia.org/wiki/Dot_product + * @param {dwv.math.Vector3D} vector3D The input vector. * @returns {number} The dot product. */ dwv.math.Vector3D.prototype.dotProduct = function (vector3D) { return (this.getX() * vector3D.getX()) + - (this.getY() * vector3D.getY()) + - (this.getZ() * vector3D.getZ()); + (this.getY() * vector3D.getY()) + + (this.getZ() * vector3D.getZ()); }; // namespaces @@ -22476,6 +25695,51 @@ dwv.tool.draw.CircleFactory.prototype.update = function ( group.add(dwv.tool.draw.getShadowCircle(circle, group)); } + // update label position + var textPos = {x: center.x, y: center.y}; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateCircleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Circle. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.CircleFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateCircleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Circle (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateCircleQuantification = function ( + group, viewController) { + // associated shape + var kcircle = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var centerPoint = new dwv.math.Point2D( + group.x() + kcircle.x(), + group.y() + kcircle.y() + ); + // circle + var circle = new dwv.math.Circle(centerPoint, kcircle.radius()); + // update text var ktext = klabel.getText(); var quantification = circle.quantify( @@ -22484,15 +25748,12 @@ dwv.tool.draw.CircleFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = {x: center.x, y: center.y}; - klabel.position(textPos); }; /** * Get the debug shadow. * - * @param {object} circle The circle to shadow. + * @param {dwv.math.Circle} circle The circle to shadow. * @param {object} group The associated group. * @returns {object} The shadow konva group. */ @@ -22531,6 +25792,7 @@ dwv.tool.draw.getShadowCircle = function (circle, group) { // namespaces var dwv = dwv || {}; dwv.tool = dwv.tool || {}; +dwv.tool.draw = dwv.tool.draw || {}; /** * The Konva namespace. * @@ -22550,7 +25812,7 @@ dwv.tool.draw.debug = false; * This tool is responsible for the draw layer group structure. The layout is: * * drawLayer - * |_ positionGroup: name="position-group", id="slice-#_frame-#"" + * |_ positionGroup: name="position-group", id="#2-0#_#3-1"" * |_ shapeGroup: name="{shape name}-group", id="#" * |_ shape: name="shape" * |_ label: name="label" @@ -22565,7 +25827,7 @@ dwv.tool.draw.debug = false; * cons: slice/frame display: 2 loops * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Draw = function (app) { /** @@ -22685,14 +25947,6 @@ dwv.tool.Draw = function (app) { */ var listeners = {}; - /** - * The associated Konva layer. - * - * @private - * @type {object} - */ - var konvaLayer = null; - /** * Handle mouse down event. * @@ -22704,14 +25958,15 @@ dwv.tool.Draw = function (app) { return; } - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var drawLayer = layerGroup.getActiveDrawLayer(); // determine if the click happened in an existing shape var stage = drawLayer.getKonvaStage(); var kshape = stage.getIntersection({ - x: event._xs, - y: event._ys + x: event._x, + y: event._y }); // update scale @@ -22726,7 +25981,7 @@ dwv.tool.Draw = function (app) { shapeEditor.disable(); shapeEditor.setShape(selectedShape); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); shapeEditor.setViewController(viewController); shapeEditor.enable(); } @@ -22742,7 +25997,9 @@ dwv.tool.Draw = function (app) { // clear array points = []; // store point - lastPoint = new dwv.math.Point2D(event._x, event._y); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); points.push(lastPoint); } }; @@ -22758,9 +26015,14 @@ dwv.tool.Draw = function (app) { return; } + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + // draw line to current pos - if (Math.abs(event._x - lastPoint.getX()) > 0 || - Math.abs(event._y - lastPoint.getY()) > 0) { + if (Math.abs(pos.x - lastPoint.getX()) > 0 || + Math.abs(pos.y - lastPoint.getY()) > 0) { // clear last added point from the list (but not the first one) // if it was marked as temporary if (points.length !== 1 && @@ -22768,22 +26030,22 @@ dwv.tool.Draw = function (app) { points.pop(); } // current point - lastPoint = new dwv.math.Point2D(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); // mark it as temporary lastPoint.tmp = true; // add it to the list points.push(lastPoint); // update points - onNewPoints(points); + onNewPoints(points, layerGroup); } }; /** * Handle mouse up event. * - * @param {object} _event The mouse up event. + * @param {object} event The mouse up event. */ - this.mouseup = function (_event) { + this.mouseup = function (event) { // exit if not started draw if (!started) { return; @@ -22797,7 +26059,9 @@ dwv.tool.Draw = function (app) { // do we have all the needed points if (points.length === currentFactory.getNPoints()) { // store points - onFinalPoints(points); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + onFinalPoints(points, layerGroup); // reset flag started = false; } else { @@ -22811,9 +26075,9 @@ dwv.tool.Draw = function (app) { /** * Handle double click event. * - * @param {object} _event The mouse up event. + * @param {object} event The mouse up event. */ - this.dblclick = function (_event) { + this.dblclick = function (event) { // exit if not started draw if (!started) { return; @@ -22825,7 +26089,9 @@ dwv.tool.Draw = function (app) { } // store points - onFinalPoints(points); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + onFinalPoints(points, layerGroup); // reset flag started = false; }; @@ -22859,14 +26125,19 @@ dwv.tool.Draw = function (app) { return; } - if (Math.abs(event._x - lastPoint.getX()) > 0 || - Math.abs(event._y - lastPoint.getY()) > 0) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + + if (Math.abs(pos.x - lastPoint.getX()) > 0 || + Math.abs(pos.y - lastPoint.getY()) > 0) { // clear last added point from the list (but not the first one) if (points.length !== 1) { points.pop(); } // current point - lastPoint = new dwv.math.Point2D(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); // add current one to the list points.push(lastPoint); // allow for anchor points @@ -22877,7 +26148,7 @@ dwv.tool.Draw = function (app) { }, currentFactory.getTimeout()); } // update points - onNewPoints(points); + onNewPoints(points, layerGroup); } }; @@ -22901,11 +26172,13 @@ dwv.tool.Draw = function (app) { event.context = 'dwv.tool.Draw'; app.onKeydown(event); } + var konvaLayer; // press delete key if (event.keyCode === 46 && shapeEditor.isActive()) { // get shape var shapeGroup = shapeEditor.getShape().getParent(); + konvaLayer = shapeGroup.getLayer(); var shapeDisplayName = dwv.tool.GetShapeDisplayName( shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0]); // delete command @@ -22918,11 +26191,11 @@ dwv.tool.Draw = function (app) { } // escape key: exit shape creation - if (event.keyCode === 27) { + if (event.keyCode === 27 && tmpShapeGroup !== null) { + konvaLayer = tmpShapeGroup.getLayer(); // reset temporary shape group - if (tmpShapeGroup) { - tmpShapeGroup.destroy(); - } + tmpShapeGroup.destroy(); + tmpShapeGroup = null; // reset flag and points started = false; points = []; @@ -22935,16 +26208,21 @@ dwv.tool.Draw = function (app) { * Update the current draw with new points. * * @param {Array} tmpPoints The array of new points. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function onNewPoints(tmpPoints) { + function onNewPoints(tmpPoints, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // remove temporary shape draw if (tmpShapeGroup) { tmpShapeGroup.destroy(); + tmpShapeGroup = null; } + // create shape group - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); tmpShapeGroup = currentFactory.create( tmpPoints, self.style, viewController); // do not listen during creation @@ -22960,18 +26238,22 @@ dwv.tool.Draw = function (app) { * Create the final shape from a point list. * * @param {Array} finalPoints The array of points. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function onFinalPoints(finalPoints) { + function onFinalPoints(finalPoints, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // reset temporary shape group if (tmpShapeGroup) { tmpShapeGroup.destroy(); + tmpShapeGroup = null; } - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); // create final shape var finalShapeGroup = currentFactory.create( @@ -22996,7 +26278,7 @@ dwv.tool.Draw = function (app) { app.addToUndoStack(command); // activate shape listeners - self.setShapeOn(finalShapeGroup); + self.setShapeOn(finalShapeGroup, layerGroup); } /** @@ -23011,42 +26293,42 @@ dwv.tool.Draw = function (app) { shapeEditor.setViewController(null); document.body.style.cursor = 'default'; // get the current draw layer - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); - konvaLayer = drawLayer.getKonvaLayer(); - activateCurrentPositionShapes(flag); + var layerGroup = app.getActiveLayerGroup(); + activateCurrentPositionShapes(flag, layerGroup); // listen to app change to update the draw layer if (flag) { - app.addEventListener('slicechange', updateDrawLayer); - app.addEventListener('framechange', updateDrawLayer); - - // init with the app window scale - this.style.setBaseScale(app.getBaseScale()); + // TODO: merge with drawController.activateDrawLayer? + app.addEventListener('positionchange', function () { + updateDrawLayer(layerGroup); + }); // same for colour this.setLineColour(this.style.getLineColour()); } else { - app.removeEventListener('slicechange', updateDrawLayer); - app.removeEventListener('framechange', updateDrawLayer); + app.removeEventListener('positionchange', function () { + updateDrawLayer(layerGroup); + }); } }; /** * Update the draw layer. + * + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function updateDrawLayer() { + function updateDrawLayer(layerGroup) { // activate the shape at current position - activateCurrentPositionShapes(true); + activateCurrentPositionShapes(true, layerGroup); } /** * Activate shapes at current position. * * @param {boolean} visible Set the draw layer visible or not. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function activateCurrentPositionShapes(visible) { - var layerController = app.getLayerController(); + function activateCurrentPositionShapes(visible, layerGroup) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); // get shape groups at the current position var shapeGroups = @@ -23056,7 +26338,7 @@ dwv.tool.Draw = function (app) { if (visible) { // activate shape listeners shapeGroups.forEach(function (group) { - self.setShapeOn(group); + self.setShapeOn(group, layerGroup); }); } else { // de-activate shape listeners @@ -23065,6 +26347,8 @@ dwv.tool.Draw = function (app) { }); } // draw + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); konvaLayer.draw(); } @@ -23087,14 +26371,15 @@ dwv.tool.Draw = function (app) { /** * Get the real position from an event. + * TODO: use layer method? * - * @param {object} index The input index. - * @returns {object} The reasl position in the image. + * @param {object} index The input index as {x,y}. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. + * @returns {object} The real position in the image as {x,y}. * @private */ - function getRealPosition(index) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + function getRealPosition(index, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); var stage = drawLayer.getKonvaStage(); return { x: stage.offset().x + index.x / stage.scale().x, @@ -23106,8 +26391,9 @@ dwv.tool.Draw = function (app) { * Set shape group on properties. * * @param {object} shapeGroup The shape group to set on. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - this.setShapeOn = function (shapeGroup) { + this.setShapeOn = function (shapeGroup, layerGroup) { // mouse over styling shapeGroup.on('mouseover', function () { document.body.style.cursor = 'pointer'; @@ -23117,6 +26403,9 @@ dwv.tool.Draw = function (app) { document.body.style.cursor = 'default'; }); + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // make it draggable shapeGroup.draggable(true); // cache drag start position @@ -23133,8 +26422,7 @@ dwv.tool.Draw = function (app) { // store colour colour = shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0].stroke(); // display trash - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var stage = drawLayer.getKonvaStage(); var scale = stage.scale(); var invscale = {x: 1 / scale.x, y: 1 / scale.y}; @@ -23149,18 +26437,22 @@ dwv.tool.Draw = function (app) { }); // drag move event handling shapeGroup.on('dragmove.draw', function (event) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); // validate the group position - dwv.tool.validateGroupPosition(drawLayer.getSize(), this); + dwv.tool.validateGroupPosition(drawLayer.getBaseSize(), this); + // update quantification if possible + if (typeof currentFactory.updateQuantification !== 'undefined') { + var vc = layerGroup.getActiveViewLayer().getViewController(); + currentFactory.updateQuantification(this, vc); + } // highlight trash when on it var offset = dwv.gui.getEventOffset(event.evt)[0]; - var eventPos = getRealPosition(offset); + var eventPos = getRealPosition(offset, layerGroup); var trashHalfWidth = trash.width() * trash.scaleX() / 2; var trashHalfHeight = trash.height() * trash.scaleY() / 2; if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth && - Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { - trash.getChildren().each(function (tshape) { + Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { + trash.getChildren().forEach(function (tshape) { tshape.stroke('orange'); }); // change the group shapes colour @@ -23169,7 +26461,7 @@ dwv.tool.Draw = function (app) { ashape.stroke('red'); }); } else { - trash.getChildren().each(function (tshape) { + trash.getChildren().forEach(function (tshape) { tshape.stroke('red'); }); // reset the group shapes colour @@ -23190,11 +26482,11 @@ dwv.tool.Draw = function (app) { trash.remove(); // delete case var offset = dwv.gui.getEventOffset(event.evt)[0]; - var eventPos = getRealPosition(offset); + var eventPos = getRealPosition(offset, layerGroup); var trashHalfWidth = trash.width() * trash.scaleX() / 2; var trashHalfHeight = trash.height() * trash.scaleY() / 2; if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth && - Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { + Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { // compensate for the drag translation this.x(dragStartPos.x); this.y(dragStartPos.y); @@ -23269,11 +26561,11 @@ dwv.tool.Draw = function (app) { }; // call client dialog if defined - if (typeof dwv.gui.openRoiDialog !== 'undefined') { - dwv.gui.openRoiDialog(ktext.meta, onSaveCallback); + if (typeof dwv.openRoiDialog !== 'undefined') { + dwv.openRoiDialog(ktext.meta, onSaveCallback); } else { // simple prompt for the text expression - var textExpr = prompt('Label', ktext.meta.textExpr); + var textExpr = dwv.prompt('Label', ktext.meta.textExpr); if (textExpr !== null) { ktext.meta.textExpr = textExpr; onSaveCallback(ktext.meta); @@ -23406,7 +26698,7 @@ dwv.tool.Draw.prototype.hasShape = function (name) { * Get the minimum position in a groups' anchors. * * @param {object} group The group that contains anchors. - * @returns {object} The minimum position. + * @returns {object} The minimum position as {x,y}. */ dwv.tool.getAnchorMin = function (group) { var anchors = group.find('.anchor'); @@ -23415,10 +26707,11 @@ dwv.tool.getAnchorMin = function (group) { } var minX = anchors[0].x(); var minY = anchors[0].y(); - anchors.each(function (anchor) { - minX = Math.min(minX, anchor.x()); - minY = Math.min(minY, anchor.y()); - }); + for (var i = 0; i < anchors.length; ++i) { + minX = Math.min(minX, anchors[i].x()); + minY = Math.min(minY, anchors[i].y()); + } + return {x: minX, y: minY}; }; @@ -23426,8 +26719,8 @@ dwv.tool.getAnchorMin = function (group) { * Bound a node position. * * @param {object} node The node to bound the position. - * @param {object} min The minimum position. - * @param {object} max The maximum position. + * @param {object} min The minimum position as {x,y}. + * @param {object} max The maximum position as {x,y}. * @returns {boolean} True if the position was corrected. */ dwv.tool.boundNodePosition = function (node, min, max) { @@ -23460,6 +26753,11 @@ dwv.tool.validateGroupPosition = function (stageSize, group) { // if anchors get mixed, width/height can be negative var shape = group.getChildren(dwv.draw.isNodeNameShape)[0]; var anchorMin = dwv.tool.getAnchorMin(group); + // handle no anchor: when dragging the label, the editor does + // not activate + if (typeof anchorMin === 'undefined') { + return null; + } var min = { x: -anchorMin.x, @@ -23539,7 +26837,7 @@ dwv.tool.GetShapeDisplayName = function (shape) { * @param {object} group The group draw. * @param {string} name The shape display name. * @param {object} layer The layer where to draw the group. - * @param {object} silent Whether to send a creation event or not. + * @param {boolean} silent Whether to send a creation event or not. * @class */ dwv.tool.DrawGroupCommand = function (group, name, layer, silent) { @@ -23853,6 +27151,7 @@ dwv.tool.DeleteGroupCommand.prototype.onUndo = function (_event) { // namespaces var dwv = dwv || {}; dwv.tool = dwv.tool || {}; +dwv.tool.draw = dwv.tool.draw || {}; /** * The Konva namespace. * @@ -24059,7 +27358,7 @@ dwv.tool.ShapeEditor = function (app) { function applyFuncToAnchors(func) { if (shape && shape.getParent()) { var anchors = shape.getParent().find('.anchor'); - anchors.each(func); + anchors.forEach(func); } } @@ -24178,10 +27477,11 @@ dwv.tool.ShapeEditor = function (app) { }); // drag move listener anchor.on('dragmove.edit', function (evt) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(evt.evt); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var drawLayer = layerGroup.getActiveDrawLayer(); // validate the anchor position - dwv.tool.validateAnchorPosition(drawLayer.getSize(), this); + dwv.tool.validateAnchorPosition(drawLayer.getBaseSize(), this); // update shape currentFactory.update(this, app.getStyle(), viewController); // redraw @@ -24525,6 +27825,52 @@ dwv.tool.draw.EllipseFactory.prototype.update = function ( group.add(dwv.tool.draw.getShadowEllipse(ellipse, group)); } + // update label position + var textPos = {x: center.x, y: center.y}; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateEllipseQuantification(group, viewController); +}; + +/** + * Update the quantification of an Ellipse. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.EllipseFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateEllipseQuantification(group, viewController); +}; + +/** + * Update the quantification of an Ellipse (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateEllipseQuantification = function ( + group, viewController) { + // associated shape + var kellipse = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var centerPoint = new dwv.math.Point2D( + group.x() + kellipse.x(), + group.y() + kellipse.y() + ); + // circle + var ellipse = new dwv.math.Ellipse( + centerPoint, kellipse.radius().x, kellipse.radius().y); + // update text var ktext = klabel.getText(); var quantification = ellipse.quantify( @@ -24533,15 +27879,12 @@ dwv.tool.draw.EllipseFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = {x: center.x, y: center.y}; - klabel.position(textPos); }; /** * Get the debug shadow. * - * @param {object} ellipse The ellipse to shadow. + * @param {dwv.math.Ellipse} ellipse The ellipse to shadow. * @param {object} group The associated group. * @returns {object} The shadow konva group. */ @@ -24587,7 +27930,7 @@ dwv.tool.filter = dwv.tool.filter || {}; * Filter tool. * * @class - * @param {object} app The associated app. + * @param {dwv.App} app The associated app. */ dwv.tool.Filter = function (app) { /** @@ -24757,7 +28100,7 @@ dwv.tool.Filter.prototype.hasFilter = function (name) { * Threshold filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Threshold = function (app) { /** @@ -24811,7 +28154,7 @@ dwv.tool.filter.Threshold = function (app) { filter.setMax(args.max); // reset the image if asked if (resetImage) { - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); resetImage = false; } var command = new dwv.tool.RunFilterCommand(filter, app); @@ -24859,7 +28202,7 @@ dwv.tool.filter.Threshold = function (app) { * Sharpen filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Sharpen = function (app) { /** @@ -24893,7 +28236,7 @@ dwv.tool.filter.Sharpen = function (app) { */ this.run = function (_args) { var filter = new dwv.image.filter.Sharpen(); - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); var command = new dwv.tool.RunFilterCommand(filter, app); command.onExecute = fireEvent; command.onUndo = fireEvent; @@ -24938,7 +28281,7 @@ dwv.tool.filter.Sharpen = function (app) { * Sobel filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Sobel = function (app) { /** @@ -24972,7 +28315,7 @@ dwv.tool.filter.Sobel = function (app) { */ dwv.tool.filter.Sobel.prototype.run = function (_args) { var filter = new dwv.image.filter.Sobel(); - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); var command = new dwv.tool.RunFilterCommand(filter, app); command.onExecute = fireEvent; command.onUndo = fireEvent; @@ -25018,7 +28361,7 @@ dwv.tool.filter.Sobel = function (app) { * * @class * @param {object} filter The filter to run. - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.RunFilterCommand = function (filter, app) { @@ -25038,9 +28381,9 @@ dwv.tool.RunFilterCommand = function (filter, app) { */ this.execute = function () { // run filter and set app image - app.setImage(filter.update()); + app.setLastImage(filter.update()); // update display - app.render(); + app.render(0); //todo: fix /** * Filter run event. * @@ -25064,9 +28407,9 @@ dwv.tool.RunFilterCommand = function (filter, app) { */ this.undo = function () { // reset the image - app.setImage(filter.getOriginalImage()); + app.setLastImage(filter.getOriginalImage()); // update display - app.render(); + app.render(0); //todo: fix /** * Filter undo event. * @@ -25116,7 +28459,7 @@ var MagicWand = MagicWand || {}; * Floodfill painting tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Floodfill = function (app) { /** @@ -25271,7 +28614,14 @@ dwv.tool.Floodfill = function (app) { * @private */ var getCoord = function (event) { - return {x: event._x, y: event._y}; + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + return { + x: index.get(0), + y: index.get(1) + }; }; /** @@ -25293,7 +28643,6 @@ dwv.tool.Floodfill = function (app) { bytes: 4 }; - // var p = new dwv.math.FastPoint2D(points.x, points.y); mask = MagicWand.floodFill(image, points.x, points.y, threshold); mask = MagicWand.gaussBlurOnlyBorder(mask, blurRadius); @@ -25322,9 +28671,10 @@ dwv.tool.Floodfill = function (app) { * @private * @param {object} point The start point. * @param {number} threshold The border threshold. + * @param {object} layerGroup The origin layer group. * @returns {boolean} False if no border. */ - var paintBorder = function (point, threshold) { + var paintBorder = function (point, threshold, layerGroup) { // Calculate the border border = calcBorder(point, threshold); // Paint the border @@ -25333,8 +28683,7 @@ dwv.tool.Floodfill = function (app) { shapeGroup = factory.create(border, self.style); shapeGroup.id(dwv.math.guid()); - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var drawController = drawLayer.getDrawController(); // get the position group @@ -25363,8 +28712,9 @@ dwv.tool.Floodfill = function (app) { * * @param {number} ini The first slice to extend to. * @param {number} end The last slice to extend to. + * @param {object} layerGroup The origin layer group. */ - this.extend = function (ini, end) { + this.extend = function (ini, end, layerGroup) { //avoid errors if (!initialpoint) { throw '\'initialpoint\' not found. User must click before use extend!'; @@ -25374,31 +28724,31 @@ dwv.tool.Floodfill = function (app) { shapeGroup.destroy(); } - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); - var pos = viewController.getCurrentPosition(); + var pos = viewController.getCurrentIndex(); + var imageSize = viewController.getImageSize(); var threshold = currentthreshold || initialthreshold; // Iterate over the next images and paint border on each slice. - for (var i = pos.k, + for (var i = pos.get(2), len = end - ? end : app.getImage().getGeometry().getSize().getNumberOfSlices(); + ? end : imageSize.get(2); i < len; i++) { - if (!paintBorder(initialpoint, threshold)) { + if (!paintBorder(initialpoint, threshold, layerGroup)) { break; } - viewController.incrementSliceNb(); + viewController.incrementIndex(2); } viewController.setCurrentPosition(pos); // Iterate over the prev images and paint border on each slice. - for (var j = pos.k, jl = ini ? ini : 0; j > jl; j--) { - if (!paintBorder(initialpoint, threshold)) { + for (var j = pos.get(2), jl = ini ? ini : 0; j > jl; j--) { + if (!paintBorder(initialpoint, threshold, layerGroup)) { break; } - viewController.decrementSliceNb(); + viewController.decrementIndex(2); } viewController.setCurrentPosition(pos); }; @@ -25452,9 +28802,10 @@ dwv.tool.Floodfill = function (app) { * @param {object} event The mouse down event. */ this.mousedown = function (event) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); imageInfo = viewLayer.getImageData(); if (!imageInfo) { @@ -25468,7 +28819,7 @@ dwv.tool.Floodfill = function (app) { self.started = true; initialpoint = getCoord(event); - paintBorder(initialpoint, initialthreshold); + paintBorder(initialpoint, initialthreshold, layerGroup); self.onThresholdChange(initialthreshold); }; @@ -25498,7 +28849,9 @@ dwv.tool.Floodfill = function (app) { this.mouseup = function (_event) { self.started = false; if (extender) { - self.extend(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + self.extend(layerGroup); } }; @@ -25837,7 +29190,7 @@ dwv.tool = dwv.tool || {}; * Livewire painting tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Livewire = function (app) { /** @@ -25915,10 +29268,11 @@ dwv.tool.Livewire = function (app) { /** * Clear the parent points list. * + * @param {object} imageSize The image size. * @private */ - function clearParentPoints() { - var nrows = app.getImage().getGeometry().getSize().getNumberOfRows(); + function clearParentPoints(imageSize) { + var nrows = imageSize.get(1); for (var i = 0; i < nrows; ++i) { parentPoints[i] = []; } @@ -25948,31 +29302,36 @@ dwv.tool.Livewire = function (app) { * @param {object} event The mouse down event. */ this.mousedown = function (event) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var imageSize = viewLayer.getViewController().getImageSize(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + // first time if (!self.started) { self.started = true; - self.x0 = event._x; - self.y0 = event._y; + self.x0 = index.get(0); + self.y0 = index.get(1); // clear vars clearPaths(); - clearParentPoints(); + clearParentPoints(imageSize); shapeGroup = null; // update zoom scale - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); self.style.setZoomScale( drawLayer.getKonvaLayer().getAbsoluteScale()); // do the training from the first point - var p = new dwv.math.FastPoint2D(event._x, event._y); + var p = {x: index.get(0), y: index.get(1)}; scissors.doTraining(p); // add the initial point to the path - var p0 = new dwv.math.Point2D(event._x, event._y); + var p0 = new dwv.math.Point2D(index.get(0), index.get(1)); path.addPoint(p0); path.addControlPoint(p0); } else { // final point: at 'tolerance' of the initial point - if ((Math.abs(event._x - self.x0) < tolerance) && - (Math.abs(event._y - self.y0) < tolerance)) { + if ((Math.abs(index.get(0) - self.x0) < tolerance) && + (Math.abs(index.get(1) - self.y0) < tolerance)) { // draw self.mousemove(event); // listen @@ -25987,8 +29346,8 @@ dwv.tool.Livewire = function (app) { } else { // anchor point path = currentPath; - clearParentPoints(); - var pn = new dwv.math.FastPoint2D(event._x, event._y); + clearParentPoints(imageSize); + var pn = {x: index.get(0), y: index.get(1)}; scissors.doTraining(pn); path.addControlPoint(currentPath.getPoint(0)); } @@ -26004,8 +29363,13 @@ dwv.tool.Livewire = function (app) { if (!self.started) { return; } + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + // set the point to find the path to - var p = new dwv.math.FastPoint2D(event._x, event._y); + var p = {x: index.get(0), y: index.get(1)}; scissors.setPoint(p); // do the work var results = 0; @@ -26053,8 +29417,7 @@ dwv.tool.Livewire = function (app) { shapeGroup = factory.create(currentPath.pointArray, self.style); shapeGroup.id(dwv.math.guid()); - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var drawController = drawLayer.getDrawController(); // get the position group @@ -26149,14 +29512,14 @@ dwv.tool.Livewire = function (app) { this.activate = function (bool) { // start scissors if displayed if (bool) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); + var layerGroup = app.getActiveLayerGroup(); + var viewLayer = layerGroup.getActiveViewLayer(); //scissors = new dwv.math.Scissors(); - var size = app.getImage().getGeometry().getSize(); + var imageSize = viewLayer.getViewController().getImageSize(); scissors.setDimensions( - size.getNumberOfColumns(), - size.getNumberOfRows()); + imageSize.get(0), + imageSize.get(1)); scissors.setData(viewLayer.getImageData().data); // init with the app window scale @@ -26235,7 +29598,7 @@ dwv.tool = dwv.tool || {}; * Opacity class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Opacity = function (app) { /** @@ -26280,8 +29643,9 @@ dwv.tool.Opacity = function (app) { var xMove = (Math.abs(diffX) > 15); // do not trigger for small moves if (xMove) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); var op = viewLayer.getOpacity(); viewLayer.setOpacity(op + (diffX / 200)); viewLayer.draw(); @@ -26967,6 +30331,58 @@ dwv.tool.draw.RectangleFactory.prototype.update = function ( kshadow.size({width: rWidth, height: rHeight}); } + // update label position + var textPos = { + x: rect.getBegin().getX() - group.x(), + y: rect.getEnd().getY() - group.y() + }; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateRectangleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Rectangle. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.RectangleFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateRectangleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Rectangle (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateRectangleQuantification = function ( + group, viewController) { + // associated shape + var krect = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var p2d0 = new dwv.math.Point2D( + group.x() + krect.x(), + group.y() + krect.y() + ); + var p2d1 = new dwv.math.Point2D( + p2d0.getX() + krect.width(), + p2d0.getY() + krect.height() + ); + // rectangle + var rect = new dwv.math.Rectangle(p2d0, p2d1); + // update text var ktext = klabel.getText(); var quantification = rect.quantify( @@ -26975,12 +30391,6 @@ dwv.tool.draw.RectangleFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = { - x: rect.getBegin().getX() - group.x(), - y: rect.getEnd().getY() - group.y() - }; - klabel.position(textPos); }; /** @@ -27510,7 +30920,7 @@ dwv.tool = dwv.tool || {}; * Scroll class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Scroll = function (app) { /** @@ -27536,9 +30946,10 @@ dwv.tool.Scroll = function (app) { */ this.mousedown = function (event) { // stop viewer if playing - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); if (viewController.isPlaying()) { viewController.stop(); } @@ -27547,6 +30958,10 @@ dwv.tool.Scroll = function (app) { // first position self.x0 = event._x; self.y0 = event._y; + + // update controller position + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + viewController.setCurrentPosition2D(planePos.x, planePos.y); }; /** @@ -27559,20 +30974,21 @@ dwv.tool.Scroll = function (app) { return; } - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); // difference to last Y position var diffY = event._y - self.y0; var yMove = (Math.abs(diffY) > 15); // do not trigger for small moves - if (yMove) { + if (yMove && viewController.canScroll()) { // update view controller if (diffY > 0) { - viewController.decrementSliceNb(); + viewController.decrementScrollIndex(); } else { - viewController.incrementSliceNb(); + viewController.incrementScrollIndex(); } } @@ -27580,12 +30996,13 @@ dwv.tool.Scroll = function (app) { var diffX = event._x - self.x0; var xMove = (Math.abs(diffX) > 15); // do not trigger for small moves - if (xMove) { + var imageSize = viewController.getImageSize(); + if (xMove && imageSize.moreThanOne(3)) { // update view controller if (diffX > 0) { - viewController.incrementFrameNb(); + viewController.decrementIndex(3); } else { - viewController.decrementFrameNb(); + viewController.incrementIndex(3); } } @@ -27667,41 +31084,21 @@ dwv.tool.Scroll = function (app) { * @param {object} event The mouse wheel event. */ this.wheel = function (event) { + var up = false; if (event.deltaY < 0) { - mouseScroll(true); - } else { - mouseScroll(false); + up = true; } - }; - /** - * Mouse scroll action. - * - * @param {boolean} up True to increment, false to decrement. - * @private - */ - function mouseScroll(up) { - var layerController = app.getLayerController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); - - var hasSlices = - (app.getImage().getGeometry().getSize().getNumberOfSlices() !== 1); - var hasFrames = (app.getImage().getNumberOfFrames() !== 1); + layerGroup.getActiveViewLayer().getViewController(); if (up) { - if (hasSlices) { - viewController.incrementSliceNb(); - } else if (hasFrames) { - viewController.incrementFrameNb(); - } + viewController.incrementScrollIndex(); } else { - if (hasSlices) { - viewController.decrementSliceNb(); - } else if (hasFrames) { - viewController.decrementFrameNb(); - } + viewController.decrementScrollIndex(); } - } + }; /** * Handle key down event. @@ -27715,12 +31112,13 @@ dwv.tool.Scroll = function (app) { /** * Handle double click. * - * @param {object} _event The key down event. + * @param {object} event The key down event. */ - this.dblclick = function (_event) { - var layerController = app.getLayerController(); + this.dblclick = function (event) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); viewController.play(); }; @@ -27923,7 +31321,7 @@ dwv.tool = dwv.tool || {}; * WindowLevel tool: handle window/level related events. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.WindowLevel = function (app) { /** @@ -27951,11 +31349,6 @@ dwv.tool.WindowLevel = function (app) { // store initial position self.x0 = event._x; self.y0 = event._y; - // update view controller - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); - viewController.setCurrentPosition2D(event._x, event._y); }; /** @@ -27969,9 +31362,10 @@ dwv.tool.WindowLevel = function (app) { return; } - var layerController = app.getLayerController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); // difference to last position var diffX = event._x - self.x0; @@ -27987,7 +31381,7 @@ dwv.tool.WindowLevel = function (app) { // add the manual preset to the view viewController.addWindowLevelPresets({ manual: { - wl: new dwv.image.WindowLevel(windowCenter, windowWidth), + wl: [new dwv.image.WindowLevel(windowCenter, windowWidth)], name: 'manual' } }); @@ -28053,16 +31447,20 @@ dwv.tool.WindowLevel = function (app) { * @param {object} event The double click event. */ this.dblclick = function (event) { - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + var viewController = viewLayer.getViewController(); + var image = app.getImage(viewLayer.getDataIndex()); // update view controller viewController.setWindowLevel( - parseInt(app.getImage().getRescaledValue( - event._x, - event._y, - viewController.getCurrentPosition().k + parseInt(image.getRescaledValueAtIndex( + viewController.getCurrentIndex().getWithNew2D( + index.get(0), + index.get(1) + ) ), 10), parseInt(viewController.getWindowLevel().width, 10)); }; @@ -28122,7 +31520,7 @@ dwv.tool = dwv.tool || {}; * ZoomAndPan class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.ZoomAndPan = function (app) { /** @@ -28147,8 +31545,8 @@ dwv.tool.ZoomAndPan = function (app) { this.mousedown = function (event) { self.started = true; // first position - self.x0 = event._xs; - self.y0 = event._ys; + self.x0 = event._x; + self.y0 = event._y; }; /** @@ -28178,13 +31576,24 @@ dwv.tool.ZoomAndPan = function (app) { return; } // calculate translation - var tx = event._xs - self.x0; - var ty = event._ys - self.y0; + var tx = event._x - self.x0; + var ty = event._y - self.y0; // apply translation - app.translate(tx, ty); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + var planeOffset = viewLayer.displayToPlaneScale(tx, ty); + var offset3D = viewController.getOffset3DFromPlaneOffset(planeOffset); + layerGroup.addTranslation({ + x: offset3D.getX(), + y: offset3D.getY(), + z: offset3D.getZ() + }); + layerGroup.draw(); // reset origin point - self.x0 = event._xs; - self.y0 = event._ys; + self.x0 = event._x; + self.y0 = event._y; }; /** @@ -28201,6 +31610,11 @@ dwv.tool.ZoomAndPan = function (app) { var newLine = new dwv.math.Line(point0, point1); var lineRatio = newLine.getLength() / self.line0.getLength(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + if (lineRatio === 1) { // scroll mode // difference to last position @@ -28209,20 +31623,23 @@ dwv.tool.ZoomAndPan = function (app) { if (Math.abs(diffY) < 15) { return; } - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var imageSize = viewController.getImageSize(); // update view controller - if (diffY > 0) { - viewController.incrementSliceNb(); - } else { - viewController.decrementSliceNb(); + if (imageSize.canScroll(2)) { + if (diffY > 0) { + viewController.incrementIndex(2); + } else { + viewController.decrementIndex(2); + } } } else { // zoom mode var zoom = (lineRatio - 1) / 2; if (Math.abs(zoom) % 0.1 <= 0.05) { - app.zoom(zoom, event._xs, event._ys); + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + var center = viewController.getPositionFromPlanePoint(planePos); + layerGroup.addScale(zoom, center); + layerGroup.draw(); } } }; @@ -28292,7 +31709,15 @@ dwv.tool.ZoomAndPan = function (app) { */ this.wheel = function (event) { var step = -event.deltaY / 500; - app.zoom(step, event._xs, event._ys); + + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + var center = viewController.getPlanePositionFromPlanePoint(planePos); + layerGroup.addScale(step, center); + layerGroup.draw(); }; /** @@ -28608,6 +32033,31 @@ dwv.utils.srgbToCielab = function (triplet) { return dwv.utils.ciexyzToCielab(dwv.utils.srgbToCiexyz(triplet)); }; +/** + * Get the hex code of a string colour for a colour used in pre dwv v0.17. + * + * @param {string} name The name of a colour. + * @returns {string} The hex representing the colour. + */ +dwv.utils.colourNameToHex = function (name) { + // default colours used in dwv version < 0.17 + var dict = { + Yellow: '#ffff00', + Red: '#ff0000', + White: '#ffffff', + Green: '#008000', + Blue: '#0000ff', + Lime: '#00ff00', + Fuchsia: '#ff00ff', + Black: '#000000' + }; + var res = '#ffff00'; + if (typeof dict[name] !== 'undefined') { + res = dict[name]; + } + return res; +}; + // namespaces var dwv = dwv || {}; /** @namespace */ @@ -29074,13 +32524,13 @@ dwv.i18nGetFallbackLocalePath = function (filename) { // namespaces var dwv = dwv || {}; -/** @namespace */ dwv.utils = dwv.utils || {}; /** * ListenerHandler class: handles add/removing and firing listeners. * * @class + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget#example */ dwv.utils.ListenerHandler = function () { /** @@ -29136,9 +32586,11 @@ dwv.utils.ListenerHandler = function () { if (typeof listeners[event.type] === 'undefined') { return; } - // fire events - for (var i = 0; i < listeners[event.type].length; ++i) { - listeners[event.type][i](event); + // fire events from a copy of the listeners array + // to avoid interference from possible add/remove + var stack = listeners[event.type].slice(); + for (var i = 0; i < stack.length; ++i) { + stack[i](event); } }; }; @@ -29147,11 +32599,15 @@ dwv.utils.ListenerHandler = function () { var dwv = dwv || {}; /** @namespace */ dwv.utils = dwv.utils || {}; +/** @namespace */ dwv.utils.logger = dwv.utils.logger || {}; +/** @namespace */ dwv.utils.logger.console = dwv.utils.logger.console || {}; /** - * Main logger, defaults to the console logger. + * Main logger namespace. Defaults to the console logger. + * + * @see dwv.utils.logger.console */ dwv.logger = dwv.utils.logger.console; @@ -30472,17 +33928,26 @@ dwv.utils.getRootPath = function (path) { }; /** - * Get a file extension + * Get a file extension: anything after the last dot. + * File name starting with a dot are discarded. + * Extensions are expected to contain at least one letter. * * @param {string} filePath The file path containing the file name. * @returns {string} The lower case file extension or null for none. */ dwv.utils.getFileExtension = function (filePath) { var ext = null; - if (typeof filePath !== 'undefined' && filePath) { - var pathSplit = filePath.split('.'); + if (typeof filePath !== 'undefined' && + filePath !== null && + filePath[0] !== '.') { + var pathSplit = filePath.toLowerCase().split('.'); if (pathSplit.length !== 1) { - ext = pathSplit.pop().toLowerCase(); + ext = pathSplit.pop(); + // extension should contain at least one letter and no slash + var regExp = /[a-z]/; + if (!regExp.test(ext) || ext.includes('/')) { + ext = null; + } } } return ext; @@ -30526,10 +33991,10 @@ dwv.utils.ThreadPool = function (poolSize) { if (freeThreads.length > 0) { // get the first free worker thread var workerThread = freeThreads.shift(); - // run the input task - workerThread.run(workerTask); // add the thread to the runnning list runningThreads.push(workerThread); + // run the input task + workerThread.run(workerTask); } else { // no free thread, add task to queue taskQueue.push(workerTask); @@ -30694,15 +34159,15 @@ dwv.utils.WorkerThread = function (parentPool) { this.run = function (workerTask) { // store task runningTask = workerTask; - // create a new web worker - if (runningTask.script !== null) { + // create a new web worker if not done yet + if (typeof worker === 'undefined') { worker = new Worker(runningTask.script); // set callbacks worker.onmessage = onmessage; worker.onerror = onerror; - // launch the worker - worker.postMessage(runningTask.startMessage); } + // launch the worker + worker.postMessage(runningTask.startMessage); }; /** @@ -30711,8 +34176,6 @@ dwv.utils.WorkerThread = function (parentPool) { this.stop = function () { // stop the worker worker.terminate(); - // tell the parent pool this thread is free - parentPool.onTaskEnd(this); }; /** @@ -30724,11 +34187,14 @@ dwv.utils.WorkerThread = function (parentPool) { * @private */ function onmessage(event) { - // pass to parent - event.index = runningTask.id; + // augment event + event.itemNumber = runningTask.info.itemNumber; + event.numberOfItems = runningTask.info.numberOfItems; + event.dataIndex = runningTask.info.dataIndex; + // send event parentPool.onworkitem(event); - // stop the worker and free the thread - self.stop(); + // tell the parent pool the task is done + parentPool.onTaskEnd(self); } /** @@ -30738,6 +34204,10 @@ dwv.utils.WorkerThread = function (parentPool) { * @private */ function onerror(event) { + // augment event + event.itemNumber = runningTask.info.itemNumber; + event.numberOfItems = runningTask.info.numberOfItems; + event.dataIndex = runningTask.info.dataIndex; // pass to parent parentPool.handleWorkerError(event); // stop the worker and free the thread @@ -30751,15 +34221,15 @@ dwv.utils.WorkerThread = function (parentPool) { * @class * @param {string} script The worker script. * @param {object} message The data to pass to the worker. - * @param {number} index The worker id. + * @param {object} info Information object about the input data. */ -dwv.utils.WorkerTask = function (script, message, index) { +dwv.utils.WorkerTask = function (script, message, info) { // worker script this.script = script; // worker start message this.startMessage = message; - // worker id - this.id = index; + // information about the work data + this.info = info; }; // namespaces @@ -31076,7 +34546,7 @@ dwv.utils.decodeManifest = function (manifest, nslices) { * Load from an input uri * * @param {string} uri The input uri, for example: 'window.location.href'. - * @param {object} app The associated app that handles the load. + * @param {dwv.App} app The associated app that handles the load. */ dwv.utils.loadFromUri = function (uri, app) { var query = dwv.utils.getUriQuery(uri); diff --git a/dist/dwv.min.js b/dist/dwv.min.js index 9ebc9c2d28..775523cf29 100644 --- a/dist/dwv.min.js +++ b/dist/dwv.min.js @@ -1,3 +1,3 @@ -/*! dwv 0.29.1 2021-06-11 17:43:54 */ +/*! dwv 0.30.0 2021-12-02 15:55:09 */ -!function(e,t){"function"==typeof define&&define.amd?define(["i18next","i18next-xhr-backend","i18next-browser-languagedetector","jszip","konva","magic-wand-tool"],t):"object"==typeof module&&module.exports?module.exports=t(require("i18next"),require("i18next-xhr-backend"),require("i18next-browser-languagedetector"),require("jszip"),require("konva"),require("magic-wand-tool")):e.dwv=t(e.i18next,e.i18nextXHRBackend,e.i18nextBrowserLanguageDetector,e.JSZip,e.Konva,e.MagicWand)}(this,function(i,r,o,c,D,p){var u=void 0!==u?u:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};void 0!==i&&void 0===i.t&&(i=i.default);var w=w||{};w.App=function(){var o=this,g=null,m=null,h=null,a=null,p=null,s=null,f=null,l=new w.gui.Style,n=new w.utils.ListenerHandler;function y(e){n.fireEvent(e)}function D(e){s=!0,"image"===e.loadtype&&m.length()===g.nSimultaneousData&&o.reset(),e.type="loadstart",y(e)}function C(e){e.type="loadprogress",y(e)}function v(e){void 0===e.data&&w.logger.error("Missing loaditem event data "+e),void 0===e.loadtype&&w.logger.error("Missing loaditem event load type "+e);var t,n,i=null,r=null;"image"===e.loadtype?(s?m.addNew(e.data.image,e.data.info):i=m.updateCurrent(e.data.image,e.data.info),r=e.data.info):"state"===e.loadtype&&((t=new w.State).apply(o,t.fromJSON(e.data)),r="state"),y({type:"loaditem",data:r,source:e.source,loadtype:e.loadtype}),"image"===e.loadtype&&(s?(a=a||new w.LayerController(o.getElement("layerContainer")),n=m.getCurrentIndex(),e=m.get(n),(0===a.getNumberOfLayers()?function(e,t,n){var i=a.addViewLayer();h&&h.hasTool("Draw")&&a.addDrawLayer();a.initialise(e,t,n),l.setBaseScale(a.getBaseScale()),u(i),a.addEventListener("zoomchange",y),a.addEventListener("offsetchange",y),m.addEventListener("imagechange",i.onimagechange),h&&h.init(a.displayToIndex)}:function(e,t,n){!function(e){e.propagateViewEvents(!1);for(var t=0;td){for(var f=p.length/d,y=[],D=0;Dn.getMax()?t:e})},w.image.filter.Sharpen=function(){this.getName=function(){return"Sharpen"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},w.image.filter.Sharpen.prototype.update=function(){return this.getOriginalImage().convolute2D([0,-1,0,-1,5,-1,0,-1,0])},w.image.filter.Sobel=function(){this.getName=function(){return"Sobel"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},w.image.filter.Sobel.prototype.update=function(){var e=this.getOriginalImage(),t=e.convolute2D([1,0,-1,2,0,-2,1,0,-1]),e=e.convolute2D([1,2,1,0,0,0,-1,-2,-1]);return t.compose(e,function(e,t){return Math.sqrt(e*e+t*t)})},(w=w||{}).image=w.image||{},w.image.Size=function(e,t,n){this.getNumberOfColumns=function(){return e},this.getNumberOfRows=function(){return t},this.getNumberOfSlices=function(){return n||1}},w.image.Size.prototype.getSliceSize=function(){return this.getNumberOfColumns()*this.getNumberOfRows()},w.image.Size.prototype.getTotalSize=function(){return this.getSliceSize()*this.getNumberOfSlices()},w.image.Size.prototype.equals=function(e){return null!==e&&this.getNumberOfColumns()===e.getNumberOfColumns()&&this.getNumberOfRows()===e.getNumberOfRows()&&this.getNumberOfSlices()===e.getNumberOfSlices()},w.image.Size.prototype.isInBounds=function(e,t,n){return!(e<0||e>this.getNumberOfColumns()-1||t<0||t>this.getNumberOfRows()-1||n<0||n>this.getNumberOfSlices()-1)},w.image.Size.prototype.toString=function(){return"("+this.getNumberOfColumns()+", "+this.getNumberOfRows()+", "+this.getNumberOfSlices()+")"},w.image.Spacing=function(e,t,n){this.getColumnSpacing=function(){return e},this.getRowSpacing=function(){return t},this.getSliceSpacing=function(){return n||1}},w.image.Spacing.prototype.equals=function(e){return null!==e&&this.getColumnSpacing()===e.getColumnSpacing()&&this.getRowSpacing()===e.getRowSpacing()&&this.getSliceSpacing()===e.getSliceSpacing()},w.image.Spacing.prototype.toString=function(){return"("+this.getColumnSpacing()+", "+this.getRowSpacing()+", "+this.getSliceSpacing()+")"},w.image.Geometry=function(e,n,t,o){var a=[e=void 0===e?new w.math.Point3D(0,0,0):e];void 0===o&&(o=new w.math.getIdentityMat33),this.getOrigin=function(){return e},this.getOrigins=function(){return a},this.getSize=function(){return n},this.getSpacing=function(){return t},this.getOrientation=function(){return o},this.getSliceIndex=function(e){for(var t,n=0,i=e.getDistance(a[0]),r=0;r>8}),a=a.map(r),S=S.map(r))):8===t[2]&&(w.logger.info("Scaling 16bits color lut since the lut descriptor is 8."),t=s.slice(0),s=new Uint8Array(t.buffer),t=a.slice(0),a=new Uint8Array(t.buffer),t=S.slice(0),S=new Uint8Array(t.buffer))),d.paletteLut={red:s,green:a,blue:S});e=e.getFromKey("x00082144");return e&&(d.RecommendedDisplayFrameRate=parseInt(e,10)),c.setMeta(d),c},(w=w||{}).image=w.image||{},w.image.range=function(t,e,n,i){void 0===i&&(i=1);var r=e;return{next:function(){if(r=2*w.image.lut.range_max/3?w.image.lut.range_max-1:0},w.image.lut.toMaxFirstThird=function(e){e*=3;return e>w.image.lut.range_max-1?w.image.lut.range_max-1:e},w.image.lut.toMaxSecondThird=function(e){var t=w.image.lut.range_max/3,n=0;return t<=e&&(n=3*(e-t))>w.image.lut.range_max-1?w.image.lut.range_max-1:n},w.image.lut.toMaxThirdThird=function(e){var t=w.image.lut.range_max/3,n=0;return 2*t<=e&&(n=3*(e-2*t))>w.image.lut.range_max-1?w.image.lut.range_max-1:n},w.image.lut.zero=function(e){return 0},w.image.lut.id=function(e){return e},w.image.lut.invId=function(e){return w.image.lut.range_max-1-e},w.image.lut.plain={red:w.image.lut.buildLut(w.image.lut.id),green:w.image.lut.buildLut(w.image.lut.id),blue:w.image.lut.buildLut(w.image.lut.id)},w.image.lut.invPlain={red:w.image.lut.buildLut(w.image.lut.invId),green:w.image.lut.buildLut(w.image.lut.invId),blue:w.image.lut.buildLut(w.image.lut.invId)},w.image.lut.rainbow={blue:[0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,247,239,231,223,215,207,199,191,183,175,167,159,151,143,135,127,119,111,103,95,87,79,71,63,55,47,39,31,23,15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,251,249,247,245,243,241,239,237,235,233,231,229,227,225,223,221,219,217,215,213,211,209,207,205,203,201,199,197,195,193,192,189,186,183,180,177,174,171,168,165,162,159,156,153,150,147,144,141,138,135,132,129,126,123,120,117,114,111,108,105,102,99,96,93,90,87,84,81,78,75,72,69,66,63,60,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3],red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,62,60,58,56,54,52,50,48,46,44,42,40,38,36,34,32,30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]},w.image.lut.hot={red:w.image.lut.buildLut(w.image.lut.toMaxFirstThird),green:w.image.lut.buildLut(w.image.lut.toMaxSecondThird),blue:w.image.lut.buildLut(w.image.lut.toMaxThirdThird)},w.image.lut.hot_iron={red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255]},w.image.lut.pet={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,128,126,124,122,120,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,76,74,72,70,68,66,64,63,61,59,57,55,53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,252,248,244,240,236,232,228,224,220,216,212,208,204,200,196,192,188,184,180,176,172,168,164,160,156,152,148,144,140,136,132,128,124,120,116,112,108,104,100,96,92,88,84,80,76,72,68,64,60,56,52,48,44,40,36,32,28,24,20,16,12,8,4,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,255]},w.image.lut.hot_metal_blue={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,85,88,90,93,96,99,102,105,108,111,114,116,119,122,125,128,131,134,137,140,143,146,149,152,155,158,161,164,166,169,172,175,178,181,184,187,190,194,198,201,205,209,213,217,221,224,228,232,236,240,244,247,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,9,11,13,15,17,19,21,23,24,26,28,30,32,34,36,38,40,41,43,45,47,49,51,53,55,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,120,122,124,126,128,130,132,134,136,137,139,141,143,145,147,149,151,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,201,203,205,207,209,211,213,215,216,218,220,222,224,226,228,229,231,233,235,237,239,240,242,244,246,248,250,251,253,255],blue:[0,2,4,6,8,10,12,14,16,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,197,194,191,188,185,182,179,176,174,171,168,165,162,159,156,153,150,144,138,132,126,121,115,109,103,97,91,85,79,74,68,62,56,50,47,44,41,38,35,32,29,26,24,21,18,15,12,9,6,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,223,226,229,232,236,239,242,245,249,252,255]},w.image.lut.pet_20step={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,192,192,192,192,176,176,176,176,176,176,176,176,176,176,176,176,176,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,96,96,96,96,96,96,96,96,96,96,96,96,96,144,144,144,144,144,144,144,144,144,144,144,144,144,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,176,176,176,176,176,176,176,176,176,176,176,176,176,144,144,144,144,144,144,144,144,144,144,144,144,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,128,176,176,176,176,176,176,176,176,176,176,176,176,176,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,64,64,64,64,64,64,64,64,64,64,64,64,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255]},w.image.lut.test={red:w.image.lut.buildLut(w.image.lut.id),green:w.image.lut.buildLut(w.image.lut.zero),blue:w.image.lut.buildLut(w.image.lut.zero)},(w=w||{}).image=w.image||{},w.image.RescaleLut=function(t,e){var n=null,i=!1,r=Math.pow(2,e);this.getRSI=function(){return t},this.isReady=function(){return i},this.initialise=function(){if(!i){n=new Float32Array(r);for(var e=0;e=a.getNumberOfFrames())&&(n!==e&&(n=e,1!==a.getNumberOfFrames()&&this.fireEvent({type:"framechange",value:[n],frame:n,skipGenerate:t})),!0)},this.setWindowLevel=function(e,t,n,i){var r,o,a;t<1||(void 0===n&&(n="manual"),void 0===i&&(i=!1),(r=new w.image.WindowLevel(e,t)).equals(l)||(o=!l||l.getWidth()!==t,a=!l||l.getCenter()!==e,l=r,s=n,o&&this.fireEvent({type:"wlwidthchange",value:[t],wc:e,ww:t,skipGenerate:i}),a&&this.fireEvent({type:"wlcenterchange",value:[e],wc:e,ww:t,skipGenerate:i})))},this.setWindowLevelPreset=function(e,t){var n=this.getWindowPresets()[e];if(void 0===n)throw new Error("Unknown window level preset: '"+e+"'");"minmax"===e&&void 0===n.wl&&(n.wl=this.getWindowLevelMinMax()),void 0!==n.perslice&&!0===n.perslice&&(n={wl:n.wl[this.getCurrentPosition().k]}),this.setWindowLevel(n.wl.getCenter(),n.wl.getWidth(),e,t)},this.setWindowLevelPresetById=function(e,t){var n=Object.keys(this.getWindowPresets());this.setWindowLevelPreset(n[e],t)},this.clone=function(){var e,t=new w.image.View(this.getImage());for(e in r)t.addWindowLut(r[e]);return t.setListeners(this.getListeners()),t};var i={};this.getListeners=function(){return i},this.setListeners=function(e){i=e}},w.image.View.prototype.getWindowLevelMinMax=function(){var e=this.getImage().getRescaledDataRange(),t=e.min,e=e.max-t;return e<1&&(w.logger.warn("Zero or negative width, defaulting to one."),e=1),new w.image.WindowLevel(t+e/2,e)},w.image.View.prototype.setWindowLevelMinMax=function(){var e=this.getWindowLevelMinMax();this.setWindowLevel(e.getCenter(),e.getWidth(),"minmax")},w.image.View.prototype.generateImageData=function(e){this.getCurrentPosition()||this.setInitialPosition();var t=this.getCurrentPosition(),n=this.getCurrentFrame(),i=this.getImage(),r=w.image.getSliceIterator(this.getImage(),t.k,n),o=i.getPhotometricInterpretation();switch(o){case"MONOCHROME1":case"MONOCHROME2":w.image.generateImageDataMonochrome(e,r,this.getCurrentWindowLut(),this.getColourMap());break;case"PALETTE COLOR":w.image.generateImageDataPaletteColor(e,r,this.getColourMap(),16===i.getMeta().BitsStored);break;case"RGB":w.image.generateImageDataRgb(e,r,this.getCurrentWindowLut());break;case"YBR_FULL":w.image.generateImageDataYbrFull(e,r);break;default:throw new Error("Unsupported photometric interpretation: "+o)}},w.image.View.prototype.addEventListener=function(e,t){var n=this.getListeners();n[e]||(n[e]=[]),n[e].push(t)},w.image.View.prototype.removeEventListener=function(e,t){var n=this.getListeners();if(n[e])for(var i=0;i>8,e.data[o+1]=n.green[r]>>8,e.data[o+2]=n.blue[r]>>8):(e.data[o]=n.red[r],e.data[o+1]=n.green[r],e.data[o+2]=n.blue[r]),e.data[o+3]=255,o+=4,a=t.next()},(w=w||{}).image=w.image||{},w.image.generateImageDataRgb=function(e,t){for(var n=0,i=t.next();!i.done;)e.data[n]=i.value[0],e.data[n+1]=i.value[1],e.data[n+2]=i.value[2],e.data[n+3]=255,n+=4,i=t.next()},(w=w||{}).image=w.image||{},w.image.generateImageDataYbrFull=function(e,t){for(var n,i=0,r=t.next();!r.done;)n=w.utils.ybrToRgb(r.value[0],r.value[1],r.value[2]),e.data[i]=n.r,e.data[i+1]=n.g,e.data[i+2]=n.b,e.data[i+3]=255,i+=4,r=t.next()},(w=w||{}).image=w.image||{},w.image.MinWindowWidth=1,w.image.validateWindowWidth=function(e){return ei.getEnd().getX()?0:-1,u=i.getBegin().getY()>i.getEnd().getY()?-1:0,c=new D.Label({x:i.getEnd().getX()+e*l.width(),y:i.getEnd().getY()+u*t.applyZoomScale(15).y,scale:t.applyZoomScale(1),visible:0!==c.length,name:"label"});c.add(l),c.add(new D.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()}));t=new D.Group;return t.name(this.getGroupName()),t.add(c),t.add(s),t.add(r),t.visible(!0),t},w.tool.draw.ArrowFactory.prototype.getAnchors=function(e,t){var n=e.points(),i=[];return i.push(w.tool.draw.getDefaultAnchor(n[0]+e.x(),n[1]+e.y(),"begin",t)),i.push(w.tool.draw.getDefaultAnchor(n[2]+e.x(),n[3]+e.y(),"end",t)),i},w.tool.draw.ArrowFactory.prototype.update=function(e,t,n){var i=e.getParent(),r=i.getChildren(function(e){return"shape"===e.name()})[0],o=i.getChildren(function(e){return"shape-triangle"===e.name()})[0],a=i.getChildren(function(e){return"label"===e.name()})[0],s=i.getChildren(function(e){return"begin"===e.id()})[0],l=i.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":s.x(e.x()),s.y(e.y());break;case"end":l.x(e.x()),l.y(e.y())}var u=s.x()-r.x(),c=s.y()-r.y(),d=l.x()-r.x(),S=l.y()-r.y();r.points([u,c,d,S]);var x=new w.math.Point2D(s.x(),s.y()),i=new w.math.Point2D(l.x(),l.y()),i=new w.math.Line(x,i),c=new w.math.Point2D(u,c),S=new w.math.Point2D(d,S),g=w.math.getPerpendicularLine(i,c,10),m=w.math.getPerpendicularLine(i,S,10);r.hitFunc(function(e){e.beginPath(),e.moveTo(g.getBegin().getX(),g.getBegin().getY()),e.lineTo(g.getEnd().getX(),g.getEnd().getY()),e.lineTo(m.getEnd().getX(),m.getEnd().getY()),e.lineTo(m.getBegin().getX(),m.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});S=new w.math.Point2D(i.getBegin().getX(),i.getBegin().getY()-10),r=new w.math.Line(i.getBegin(),S),S=w.math.getAngle(i,r),r=S*Math.PI/180;o.x(i.getBegin().getX()+o.radius()*Math.sin(r)),o.y(i.getBegin().getY()+o.radius()*Math.cos(r)),o.rotation(-S);r=a.getText();r.setText(r.meta.textExpr);o=i.getBegin().getX()>i.getEnd().getX()?0:-1,S=i.getBegin().getY()>i.getEnd().getY()?-1:0,t={x:i.getEnd().getX()+o*r.width(),y:i.getEnd().getY()+S*t.applyZoomScale(15).y};a.position(t)},(w=w||{}).tool=w.tool||{},w.tool.draw=w.tool.draw||{};D=D||{};w.tool.draw.defaultCircleLabelText="{surface}",w.tool.draw.CircleFactory=function(){this.getGroupName=function(){return"circle-group"},this.getNPoints=function(){return 2},this.getTimeout=function(){return 0}},w.tool.draw.CircleFactory.prototype.isFactoryGroup=function(e){return this.getGroupName()===e.name()},w.tool.draw.CircleFactory.prototype.create=function(e,t,n){var i=Math.abs(e[0].getX()-e[1].getX()),r=Math.abs(e[0].getY()-e[1].getY()),o=Math.round(Math.sqrt(i*i+r*r)),i=new w.math.Circle(e[0],o),r=new D.Circle({x:i.getCenter().getX(),y:i.getCenter().getY(),radius:i.getRadius(),stroke:t.getLineColour(),strokeWidth:t.getStrokeWidth(),strokeScaleEnabled:!1,name:"shape"}),e=new D.Text({fontSize:t.getFontSize(),fontFamily:t.getFontFamily(),fill:t.getLineColour(),padding:t.getTextPadding(),shadowColor:t.getShadowLineColour(),shadowOffset:t.getShadowOffset(),name:"text"}),o="",o=void 0!==w.tool.draw.circleLabelText?w.tool.draw.circleLabelText:w.tool.draw.defaultCircleLabelText,n=i.quantify(n,w.utils.getFlags(o));e.setText(w.utils.replaceFlags(o,n)),e.meta={textExpr:o,quantification:n};var a,o=new D.Label({x:i.getCenter().getX(),y:i.getCenter().getY(),scale:t.applyZoomScale(1),visible:0!==o.length,name:"label"});o.add(e),o.add(new D.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()})),w.tool.draw.debug&&(a=w.tool.draw.getShadowCircle(i));i=new D.Group;return i.name(this.getGroupName()),a&&i.add(a),i.add(o),i.add(r),i.visible(!0),i},w.tool.draw.CircleFactory.prototype.getAnchors=function(e,t){var n=e.x(),i=e.y(),r=e.radius(),e=[];return e.push(w.tool.draw.getDefaultAnchor(n-r,i,"left",t)),e.push(w.tool.draw.getDefaultAnchor(n+r,i,"right",t)),e.push(w.tool.draw.getDefaultAnchor(n,i-r,"bottom",t)),e.push(w.tool.draw.getDefaultAnchor(n,i+r,"top",t)),e},w.tool.draw.CircleFactory.prototype.update=function(e,t,n){var i,r=e.getParent(),o=r.getChildren(function(e){return"shape"===e.name()})[0],a=r.getChildren(function(e){return"label"===e.name()})[0],s=r.getChildren(function(e){return"left"===e.id()})[0],l=r.getChildren(function(e){return"right"===e.id()})[0],u=r.getChildren(function(e){return"bottom"===e.id()})[0],c=r.getChildren(function(e){return"top"===e.id()})[0];w.tool.draw.debug&&(i=r.getChildren(function(e){return"shadow"===e.name()})[0]);var d,S={x:o.x(),y:o.y()};switch(e.id()){case"left":d=S.x-e.x(),s.y(l.y()),l.x(S.x+d),u.y(S.y-d),c.y(S.y+d);break;case"right":d=e.x()-S.x,l.y(s.y()),s.x(S.x-d),u.y(S.y-d),c.y(S.y+d);break;case"bottom":d=S.y-e.y(),u.x(c.x()),s.x(S.x-d),l.x(S.x+d),c.y(S.y+d);break;case"top":d=e.y()-S.y,c.x(u.x()),s.x(S.x-d),l.x(S.x+d),u.y(S.y-d);break;default:w.logger.error("Unhandled anchor id: "+e.id())}o.radius(Math.abs(d));o=new w.math.Point2D(r.x()+S.x,r.y()+S.y),o=new w.math.Circle(o,d);i&&(i.destroy(),r.add(w.tool.draw.getShadowCircle(o,r)));r=a.getText(),n=o.quantify(n,w.utils.getFlags(r.meta.textExpr));r.setText(w.utils.replaceFlags(r.meta.textExpr,n)),r.meta.quantification=n,a.position({x:S.x,y:S.y})},w.tool.draw.getShadowCircle=function(e,t){var n=0,i=0;void 0!==t&&(n=t.x(),i=t.y());var r=new D.Group;r.name("shadow");for(var o=e.getRound(),a=0;an.x&&(e.x(n.x),i=!0),e.y()n.y&&(e.y(n.y),i=!0),i},w.tool.validateGroupPosition=function(e,t){var n=t.getChildren(w.draw.isNodeNameShape)[0],i=w.tool.getAnchorMin(t),r={x:-i.x,y:-i.y},n={x:e.x-(i.x+Math.abs(n.width())),y:e.y-(i.y+Math.abs(n.height()))};return w.tool.boundNodePosition(t,r,n)},w.tool.validateAnchorPosition=function(e,t){var n=t.getParent(),i={x:-n.x(),y:-n.y()},n={x:e.x-n.x(),y:e.y-n.y()};return w.tool.boundNodePosition(t,i,n)},(w=w||{}).tool=w.tool||{};D=D||{};w.tool.GetShapeDisplayName=function(e){var t="shape";return e instanceof D.Line?t=4===e.points().length?"line":6===e.points().length?"protractor":"roi":e instanceof D.Rect?t="rectangle":e instanceof D.Ellipse&&(t="ellipse"),t},w.tool.DrawGroupCommand=function(e,t,n,i){var r=void 0!==i&&i,o=e.getParent();this.getName=function(){return"Draw-"+t},this.execute=function(){o.add(e),n.draw(),r||this.onExecute({type:"drawcreate",id:e.id()})},this.undo=function(){e.remove(),n.draw(),this.onUndo({type:"drawdelete",id:e.id()})}},w.tool.DrawGroupCommand.prototype.onExecute=function(e){},w.tool.DrawGroupCommand.prototype.onUndo=function(e){},w.tool.MoveGroupCommand=function(t,e,n,i){this.getName=function(){return"Move-"+e},this.execute=function(){t.move(n),i.draw(),this.onExecute({type:"drawmove",id:t.id()})},this.undo=function(){var e={x:-n.x,y:-n.y};t.move(e),i.draw(),this.onUndo({type:"drawmove",id:t.id()})}},w.tool.MoveGroupCommand.prototype.onExecute=function(e){},w.tool.MoveGroupCommand.prototype.onUndo=function(e){},w.tool.ChangeGroupCommand=function(e,t,n,i,r,o,a){this.getName=function(){return"Change-"+e},this.execute=function(){t(i,a,o),r.draw(),this.onExecute({type:"drawchange"})},this.undo=function(){t(n,a,o),r.draw(),this.onUndo({type:"drawchange"})}},w.tool.ChangeGroupCommand.prototype.onExecute=function(e){},w.tool.ChangeGroupCommand.prototype.onUndo=function(e){},w.tool.DeleteGroupCommand=function(e,t,n){var i=e.getParent();this.getName=function(){return"Delete-"+t},this.execute=function(){e.remove(),n.draw(),this.onExecute({type:"drawdelete",id:e.id()})},this.undo=function(){i.add(e),n.draw(),this.onUndo({type:"drawcreate",id:e.id()})}},w.tool.DeleteGroupCommand.prototype.onExecute=function(e){},w.tool.DeleteGroupCommand.prototype.onUndo=function(e){},(w=w||{}).tool=w.tool||{};D=D||{};w.tool.draw.getDefaultAnchor=function(e,t,n,i){return new D.Ellipse({x:e,y:t,stroke:"#999",fill:"rgba(100,100,100,0.7",strokeWidth:i.getStrokeWidth(),strokeScaleEnabled:!1,radius:i.applyZoomScale(3),name:"anchor",id:n,dragOnTop:!1,draggable:!0,visible:!1})},w.tool.ShapeEditor=function(o){var a=null,s=null,l=null,u=null,e=!1,c=null;function n(e){l&&l.getParent()&&l.getParent().find(".anchor").each(e)}function t(t){n(function(e){e.visible(t)})}function d(){n(function(e){e.remove()})}function S(){if(l&&l.getLayer())for(var e=l.getParent(),t=s.getAnchors(l,o.getStyle()),n=0;ni.getEnd().getX()?0:-1,o=i.getBegin().getY()>i.getEnd().getY()?-1:0,e=new D.Label({x:i.getEnd().getX()+n*c.width(),y:i.getEnd().getY()+o*t.applyZoomScale(15).y,scale:t.applyZoomScale(1),visible:0!==e.length,name:"label"});e.add(c),e.add(new D.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()}));t=new D.Group;return t.name(this.getGroupName()),t.add(e),t.add(s),t.add(u),t.add(r),t.visible(!0),t},w.tool.draw.RulerFactory.prototype.getAnchors=function(e,t){var n=e.points(),i=[];return i.push(w.tool.draw.getDefaultAnchor(n[0]+e.x(),n[1]+e.y(),"begin",t)),i.push(w.tool.draw.getDefaultAnchor(n[2]+e.x(),n[3]+e.y(),"end",t)),i},w.tool.draw.RulerFactory.prototype.update=function(e,t,n){var i=e.getParent(),r=i.getChildren(function(e){return"shape"===e.name()})[0],o=i.getChildren(function(e){return"shape-tick0"===e.name()})[0],a=i.getChildren(function(e){return"shape-tick1"===e.name()})[0],s=i.getChildren(function(e){return"label"===e.name()})[0],l=i.getChildren(function(e){return"begin"===e.id()})[0],u=i.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":l.x(e.x()),l.y(e.y());break;case"end":u.x(e.x()),u.y(e.y())}var c=l.x()-r.x(),d=l.y()-r.y(),S=u.x()-r.x(),x=u.y()-r.y();r.points([c,d,S,x]);var g=new w.math.Point2D(l.x(),l.y()),i=new w.math.Point2D(u.x(),u.y()),i=new w.math.Line(g,i),d=new w.math.Point2D(c,d),x=new w.math.Point2D(S,x),m=w.math.getPerpendicularLine(i,d,t.scale(10));o.points([m.getBegin().getX(),m.getBegin().getY(),m.getEnd().getX(),m.getEnd().getY()]);var h=w.math.getPerpendicularLine(i,x,t.scale(10));a.points([h.getBegin().getX(),h.getBegin().getY(),h.getEnd().getX(),h.getEnd().getY()]),r.hitFunc(function(e){e.beginPath(),e.moveTo(m.getBegin().getX(),m.getBegin().getY()),e.lineTo(m.getEnd().getX(),m.getEnd().getY()),e.lineTo(h.getEnd().getX(),h.getEnd().getY()),e.lineTo(h.getBegin().getX(),h.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});a=s.getText(),r=i.quantify(n,w.utils.getFlags(a.meta.textExpr));a.setText(w.utils.replaceFlags(a.meta.textExpr,r)),a.meta.quantification=r;n=i.getBegin().getX()>i.getEnd().getX()?0:-1,r=i.getBegin().getY()>i.getEnd().getY()?-1:0,t={x:i.getEnd().getX()+n*a.width(),y:i.getEnd().getY()+r*t.applyZoomScale(15).y};s.position(t)},(w=w||{}).tool=w.tool||{},w.tool.Scroll=function(o){var a=this;this.started=!1;var t=null;function n(e){var t=o.getLayerController().getActiveViewLayer().getViewController(),n=1!==o.getImage().getGeometry().getSize().getNumberOfSlices(),i=1!==o.getImage().getNumberOfFrames();e?n?t.incrementSliceNb():i&&t.incrementFrameNb():n?t.decrementSliceNb():i&&t.decrementFrameNb()}this.mousedown=function(e){var t=o.getLayerController().getActiveViewLayer().getViewController();t.isPlaying()&&t.stop(),a.started=!0,a.x0=e._x,a.y0=e._y},this.mousemove=function(e){var t,n,i,r;a.started&&(t=o.getLayerController().getActiveViewLayer().getViewController(),r=e._y-a.y0,(n=15>>0;if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var i=arguments[1],r=0;r=e.get(3))&&r.image.appendFrame(),r.image.appendSlice(t,i),0===i&&(i="",i=void 0!==n.x00020010?"InstanceNumber":"imageUid",r.meta=A.utils.mergeObjects(r.meta,a(n),i,"value"))},this.addEventListener=function(e,t){n.add(e,t)},this.removeEventListener=function(e,t){n.remove(e,t)}},(A=A||{}).draw=A.draw||{},A.ctrl=A.ctrl||{};y=y||{};A.draw.getDrawPositionGroupId=function(e){return"slice-"+e.get(2)+"_frame-"+(4===e.length()?e.get(3):0)},A.draw.getPositionFromGroupId=function(e){var t=e.indexOf("_");return-1===t&&A.logger.warn("Badly formed PositionGroupId: "+e),{sliceNumber:e.substring(6,t),frameNumber:e.substring(t+7)}},A.draw.isNodeNameShape=function(e){return"shape"===e.name()},A.draw.isNodeNameShapeExtra=function(e){return e.name().startsWith("shape-")},A.draw.isNodeNameLabel=function(e){return"label"===e.name()},A.draw.isPositionNode=function(e){return"position-group"===e.name()},A.draw.isNodeWithId=function(t){return function(e){return e.id()===t}},A.draw.canNodeChangeColour=function(e){return"anchor"!==e.name()&&"label"!==e.name()},A.draw.getHierarchyLog=function(e,t){void 0===t&&(t="");for(var n=e.getChildren(),i=t+"|__ "+e.name()+": "+e.id()+"\n",r=0;r2^"+t+")."))}return i},A.dicom.is32bitVLVR=function(e){return"OB"===e||"OW"===e||"OF"===e||"ox"===e||"UT"===e||"SQ"===e||"UN"===e},A.dicom.getDataElementPrefixByteSize=function(e,t){return!t&&A.dicom.is32bitVLVR(e)?12:8},A.dicom.DicomParser=function(){var t;this.dicomElements={},this.getDefaultCharacterSet=function(){return t},this.setDefaultCharacterSet=function(e){t=e}},A.dicom.DicomParser.prototype.getRawDicomElements=function(){return this.dicomElements},A.dicom.DicomParser.prototype.getDicomElements=function(){return new A.dicom.DicomElementsWrapper(this.dicomElements)},A.dicom.DicomParser.prototype.readTag=function(e,t){var n=e.readHex(t);t+=Uint16Array.BYTES_PER_ELEMENT;e=e.readHex(t);return t+=Uint16Array.BYTES_PER_ELEMENT,{group:n,element:e,name:new A.dicom.Tag(n,e).getKey(),endOffset:t}},A.dicom.DicomParser.prototype.readExplicitItemDataElement=function(e,t,n){var i={},r=this.readDataElement(e,t,n),o=t=r.endOffset;for(t-=(i[r.tag.name]=r).vl;tg){for(var x=d.length/g,S=[],m=0;m2^"+i+") for decompressed data.")),d.abort(),g.onerror({error:e,source:origin}),g.onloadend({source:origin})}}n.length!==m[t]&&A.logger.warn("Unsupported varying decompressed data size: "+n.length+" != "+m[t]),S[t].set(n,m[t]*e.itemNumber)}else S[t]=n;0===e.itemNumber&&h(t,origin)}(e),e.itemNumber+1===e.numberOfItems&&(g.onload(e),g.onloadend(e))},d.onerror=g.onerror,d.onabort=g.onabort);for(var u=0;un.getMax()?t:e})},A.image.filter.Sharpen=function(){this.getName=function(){return"Sharpen"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},A.image.filter.Sharpen.prototype.update=function(){return this.getOriginalImage().convolute2D([0,-1,0,-1,5,-1,0,-1,0])},A.image.filter.Sobel=function(){this.getName=function(){return"Sobel"};var t=null;this.setOriginalImage=function(e){t=e},this.getOriginalImage=function(){return t}},A.image.filter.Sobel.prototype.update=function(){var e=this.getOriginalImage(),t=e.convolute2D([1,0,-1,2,0,-2,1,0,-1]),e=e.convolute2D([1,2,1,0,0,0,-1,-2,-1]);return t.compose(e,function(e,t){return Math.sqrt(e*e+t*t)})},(A=A||{}).image=A.image||{},A.image.Geometry=function(e,n,i,l){var u=[e=void 0===e?new A.math.Point3D(0,0,0):e];void 0===l&&(l=new A.math.getIdentityMat33);var r=!1;this.getOrigin=function(){return u[u.length-1]},this.getOrigins=function(){return u},this.getSize=function(e){var t=n;return e&&void 0!==e&&(e=A.math.getOrientedArray3D([n.get(0),n.get(1),n.get(2)],e),t=new A.image.Size(e)),t},this.getSliceGeometrySpacing=function(){if(1===u.length)return 1;for(var e,t=null,n=l.getInverse().asOneAndZeros(),i=[],r=0;r=L.numberOfFiles)throw new Error("Cannot append a frame at an index above the number of frames");y.set(e,i*t),this.appendFrame()},this.appendFrame=function(){p.appendFrame(),u.fireEvent({type:"appendframe"})},this.getDataRange=function(){return n=n||this.calculateDataRange()},this.getRescaledDataRange=function(){return i=i||this.calculateRescaledDataRange()},this.getHistogram=function(){var e;return l||(e=this.calculateHistogram(),n=e.dataRange,i=e.rescaledDataRange,l=e.histogram),l},this.addEventListener=function(e,t){u.add(e,t)},this.removeEventListener=function(e,t){u.remove(e,t)}},A.image.Image.prototype.getValue=function(e,t,n,i){i=new A.math.Index([e,t,n,i||0]);return this.getValueAtOffset(this.getGeometry().getSize().indexToOffset(i))},A.image.Image.prototype.getValueAtIndex=function(e){return this.getValueAtOffset(this.getGeometry().getSize().indexToOffset(e))},A.image.Image.prototype.getRescaledValue=function(e,t,n,i){var r=this.getValue(e,t,n,i=void 0===i?0:i);return r=!this.isIdentityRSI()?this.isConstantRSI()?this.getRescaleSlopeAndIntercept().apply(r):(i=new A.math.Index([e,t,n,i]),this.getRescaleSlopeAndIntercept(i).apply(r)):r},A.image.Image.prototype.getRescaledValueAtIndex=function(e){return this.getRescaledValueAtOffset(this.getGeometry().getSize().indexToOffset(e))},A.image.Image.prototype.getRescaledValueAtOffset=function(e){var t=this.getValueAtOffset(e);return t=!this.isIdentityRSI()?this.isConstantRSI()?this.getRescaleSlopeAndIntercept().apply(t):(e=this.getGeometry().getSize().offsetToIndex(e),this.getRescaleSlopeAndIntercept(e).apply(t)):t},A.image.Image.prototype.calculateDataRange=function(){var e,t=this.getValueAtOffset(0),n=t,i=this.getGeometry().getSize(),r=i.getTotalSize();3<=i.length()&&(r=i.getDimSize(3));for(var o=0;o>8}),g=g.map(t),i=i.map(t))):8===a[2]&&(A.logger.info("Scaling 16bits color lut since the lut descriptor is 8."),a=n.slice(0),n=new Uint8Array(a.buffer),a=g.slice(0),g=new Uint8Array(a.buffer),a=i.slice(0),i=new Uint8Array(a.buffer))),x.paletteLut={red:n,green:g,blue:i});e=e.getFromKey("x00082144");return e&&(x.RecommendedDisplayFrameRate=parseInt(e,10)),u.setMeta(x),u},(A=A||{}).image=A.image||{},A.image.simpleRange=function(t,e,n,i){void 0===i&&(i=1);var r=e;return{next:function(){if(r=2*A.image.lut.range_max/3?A.image.lut.range_max-1:0},A.image.lut.toMaxFirstThird=function(e){e*=3;return e>A.image.lut.range_max-1?A.image.lut.range_max-1:e},A.image.lut.toMaxSecondThird=function(e){var t=A.image.lut.range_max/3,n=0;return t<=e&&(n=3*(e-t))>A.image.lut.range_max-1?A.image.lut.range_max-1:n},A.image.lut.toMaxThirdThird=function(e){var t=A.image.lut.range_max/3,n=0;return 2*t<=e&&(n=3*(e-2*t))>A.image.lut.range_max-1?A.image.lut.range_max-1:n},A.image.lut.zero=function(e){return 0},A.image.lut.id=function(e){return e},A.image.lut.invId=function(e){return A.image.lut.range_max-1-e},A.image.lut.plain={red:A.image.lut.buildLut(A.image.lut.id),green:A.image.lut.buildLut(A.image.lut.id),blue:A.image.lut.buildLut(A.image.lut.id)},A.image.lut.invPlain={red:A.image.lut.buildLut(A.image.lut.invId),green:A.image.lut.buildLut(A.image.lut.invId),blue:A.image.lut.buildLut(A.image.lut.invId)},A.image.lut.rainbow={blue:[0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,247,239,231,223,215,207,199,191,183,175,167,159,151,143,135,127,119,111,103,95,87,79,71,63,55,47,39,31,23,15,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128,136,144,152,160,168,176,184,192,200,208,216,224,232,240,248,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,251,249,247,245,243,241,239,237,235,233,231,229,227,225,223,221,219,217,215,213,211,209,207,205,203,201,199,197,195,193,192,189,186,183,180,177,174,171,168,165,162,159,156,153,150,147,144,141,138,135,132,129,126,123,120,117,114,111,108,105,102,99,96,93,90,87,84,81,78,75,72,69,66,63,60,57,54,51,48,45,42,39,36,33,30,27,24,21,18,15,12,9,6,3],red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,62,60,58,56,54,52,50,48,46,44,42,40,38,36,34,32,30,28,26,24,22,20,18,16,14,12,10,8,6,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255]},A.image.lut.hot={red:A.image.lut.buildLut(A.image.lut.toMaxFirstThird),green:A.image.lut.buildLut(A.image.lut.toMaxSecondThird),blue:A.image.lut.buildLut(A.image.lut.toMaxThirdThird)},A.image.lut.hot_iron={red:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,255]},A.image.lut.pet={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,128,126,124,122,120,118,116,114,112,110,108,106,104,102,100,98,96,94,92,90,88,86,84,82,80,78,76,74,72,70,68,66,64,63,61,59,57,55,53,51,49,47,45,43,41,39,37,35,33,31,29,27,25,23,21,19,17,15,13,11,9,7,5,3,1,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184,186,188,190,192,194,196,198,200,202,204,206,208,210,212,214,216,218,220,222,224,226,228,230,232,234,236,238,240,242,244,246,248,250,252,255],blue:[0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,185,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,219,221,223,225,227,229,231,233,235,237,239,241,243,245,247,249,251,253,255,252,248,244,240,236,232,228,224,220,216,212,208,204,200,196,192,188,184,180,176,172,168,164,160,156,152,148,144,140,136,132,128,124,120,116,112,108,104,100,96,92,88,84,80,76,72,68,64,60,56,52,48,44,40,36,32,28,24,20,16,12,8,4,0,4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,85,89,93,97,101,105,109,113,117,121,125,129,133,137,141,145,149,153,157,161,165,170,174,178,182,186,190,194,198,202,206,210,214,218,222,226,230,234,238,242,246,250,255]},A.image.lut.hot_metal_blue={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,52,55,57,59,62,64,66,69,71,74,76,78,81,83,85,88,90,93,96,99,102,105,108,111,114,116,119,122,125,128,131,134,137,140,143,146,149,152,155,158,161,164,166,169,172,175,178,181,184,187,190,194,198,201,205,209,213,217,221,224,228,232,236,240,244,247,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,6,8,9,11,13,15,17,19,21,23,24,26,28,30,32,34,36,38,40,41,43,45,47,49,51,53,55,56,58,60,62,64,66,68,70,72,73,75,77,79,81,83,85,87,88,90,92,94,96,98,100,102,104,105,107,109,111,113,115,117,119,120,122,124,126,128,130,132,134,136,137,139,141,143,145,147,149,151,152,154,156,158,160,162,164,166,168,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,201,203,205,207,209,211,213,215,216,218,220,222,224,226,228,229,231,233,235,237,239,240,242,244,246,248,250,251,253,255],blue:[0,2,4,6,8,10,12,14,16,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79,81,83,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,117,119,121,123,125,127,129,131,133,135,137,139,141,143,145,147,149,151,153,155,157,159,161,163,165,167,169,171,173,175,177,179,181,183,184,186,188,190,192,194,196,198,200,197,194,191,188,185,182,179,176,174,171,168,165,162,159,156,153,150,144,138,132,126,121,115,109,103,97,91,85,79,74,68,62,56,50,47,44,41,38,35,32,29,26,24,21,18,15,12,9,6,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,6,9,12,15,18,21,24,26,29,32,35,38,41,44,47,50,53,56,59,62,65,68,71,74,76,79,82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,126,129,132,135,138,141,144,147,150,153,156,159,162,165,168,171,174,176,179,182,185,188,191,194,197,200,203,206,210,213,216,219,223,226,229,232,236,239,242,245,249,252,255]},A.image.lut.pet_20step={red:[0,0,0,0,0,0,0,0,0,0,0,0,0,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,208,192,192,192,192,192,192,192,192,192,192,192,192,192,176,176,176,176,176,176,176,176,176,176,176,176,176,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255],green:[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,112,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,96,96,96,96,96,96,96,96,96,96,96,96,96,144,144,144,144,144,144,144,144,144,144,144,144,144,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,224,208,208,208,208,208,208,208,208,208,208,208,208,208,176,176,176,176,176,176,176,176,176,176,176,176,176,144,144,144,144,144,144,144,144,144,144,144,144,96,96,96,96,96,96,96,96,96,96,96,96,96,48,48,48,48,48,48,48,48,48,48,48,48,48,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255],blue:[0,0,0,0,0,0,0,0,0,0,0,0,0,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,80,112,112,112,112,112,112,112,112,112,112,112,112,128,128,128,128,128,128,128,128,128,128,128,128,128,176,176,176,176,176,176,176,176,176,176,176,176,176,192,192,192,192,192,192,192,192,192,192,192,192,192,224,224,224,224,224,224,224,224,224,224,224,224,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,80,80,80,80,80,80,80,80,80,80,80,80,80,64,64,64,64,64,64,64,64,64,64,64,64,80,80,80,80,80,80,80,80,80,80,80,80,80,96,96,96,96,96,96,96,96,96,96,96,96,96,64,64,64,64,64,64,64,64,64,64,64,64,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255]},A.image.lut.test={red:A.image.lut.buildLut(A.image.lut.id),green:A.image.lut.buildLut(A.image.lut.zero),blue:A.image.lut.buildLut(A.image.lut.zero)},(A=A||{}).image=A.image||{},A.image.PlaneHelper=function(t,i){this.getOffset3DFromPlaneOffset=function(e){e=new A.math.Vector3D(e.x,e.y,0),e=this.getDeOrientedVector3D(e);return e=e,new A.math.Vector3D(e.getX()*t.get(0),e.getY()*t.get(1),e.getZ()*t.get(2))},this.getPlaneOffsetFromOffset3D=function(e){e=e,e=new A.math.Vector3D(e.x/t.get(0),e.y/t.get(1),e.z/t.get(2)),e=this.getOrientedVector3D(e);return{x:e.getX(),y:e.getY()}},this.getOrientedVector3D=function(e){var t=e;return t=void 0!==i?i.getInverse().getAbs().multiplyVector3D(e):t},this.getOrientedIndex=function(e){var t=e;return t=void 0!==i?i.getInverse().getAbs().multiplyIndex3D(e):t},this.getOrientedPoint=function(e){var t,n=e;return void 0!==i&&(t=i.getInverse().getAbs().multiplyPoint3D(e.get3D()),n=e.mergeWith3D(t)),n},this.getDeOrientedVector3D=function(e){var t=e;return t=void 0!==i?i.getAbs().multiplyVector3D(e):t},this.getOrientedXYZ=function(e){e=A.math.getOrientedArray3D([e.x,e.y,e.z],i);return{x:e[0],y:e[1],z:e[2]}},this.getDeOrientedXYZ=function(e){e=A.math.getDeOrientedArray3D([e.x,e.y,e.z],i);return{x:e[0],y:e[1],z:e[2]}},this.getScrollIndex=function(){return void 0!==i?i.getThirdColMajorDirection():2}},(A=A||{}).image=A.image||{},A.image.RescaleLut=function(t,e){var n=null,i=!1,r=Math.pow(2,e);this.getRSI=function(){return t},this.isReady=function(){return i},this.initialise=function(){if(!i){n=new Float32Array(r);for(var e=0;e=e+1&&1!==this.get(e)},A.image.Size.prototype.canScroll=function(e){var t=2;return void 0!==e&&(t=e.getThirdColMajorDirection()),this.moreThanOne(t)},A.image.Size.prototype.getDimSize=function(e,t){if(e>this.length())return null;if(void 0===t)t=0;else if(t<0||ethis.get(n)-1)return!1;return!0},A.image.Size.prototype.indexToOffset=function(e,t){if(e.length()this.length()-1)throw new Error("Invalid start value for indexToOffset");for(var n=0,i=t;i>8,e.data[a+1]=i.green[o]>>8,e.data[a+2]=i.blue[o]>>8):(e.data[a]=i.red[o],e.data[a+1]=i.green[o],e.data[a+2]=i.blue[o]),e.data[a+3]=n(o),a+=4,s=t.next()},(A=A||{}).image=A.image||{},A.image.generateImageDataRgb=function(e,t,n){for(var i=0,r=t.next();!r.done;)e.data[i]=r.value[0],e.data[i+1]=r.value[1],e.data[i+2]=r.value[2],e.data[i+3]=n(r.value),i+=4,r=t.next()},(A=A||{}).image=A.image||{},A.image.generateImageDataYbrFull=function(e,t,n){for(var i,r=0,o=t.next();!o.done;)i=A.utils.ybrToRgb(o.value[0],o.value[1],o.value[2]),e.data[r]=i.r,e.data[r+1]=i.g,e.data[r+2]=i.b,e.data[r+3]=n(o.value),r+=4,o=t.next()},(A=A||{}).image=A.image||{},A.image.MinWindowWidth=1,A.image.validateWindowWidth=function(e){return e=this.length())throw new Error("Non valid dimension for toStringId.");for(var i="",r=0;ri.getEnd().getX()?0:-1,u=i.getBegin().getY()>i.getEnd().getY()?-1:0,c=new y.Label({x:i.getEnd().getX()+e*l.width(),y:i.getEnd().getY()+u*t.applyZoomScale(15).y,scale:t.applyZoomScale(1),visible:0!==c.length,name:"label"});c.add(l),c.add(new y.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()}));t=new y.Group;return t.name(this.getGroupName()),t.add(c),t.add(s),t.add(r),t.visible(!0),t},A.tool.draw.ArrowFactory.prototype.getAnchors=function(e,t){var n=e.points(),i=[];return i.push(A.tool.draw.getDefaultAnchor(n[0]+e.x(),n[1]+e.y(),"begin",t)),i.push(A.tool.draw.getDefaultAnchor(n[2]+e.x(),n[3]+e.y(),"end",t)),i},A.tool.draw.ArrowFactory.prototype.update=function(e,t,n){var i=e.getParent(),r=i.getChildren(function(e){return"shape"===e.name()})[0],o=i.getChildren(function(e){return"shape-triangle"===e.name()})[0],a=i.getChildren(function(e){return"label"===e.name()})[0],s=i.getChildren(function(e){return"begin"===e.id()})[0],l=i.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":s.x(e.x()),s.y(e.y());break;case"end":l.x(e.x()),l.y(e.y())}var u=s.x()-r.x(),c=s.y()-r.y(),g=l.x()-r.x(),d=l.y()-r.y();r.points([u,c,g,d]);var x=new A.math.Point2D(s.x(),s.y()),i=new A.math.Point2D(l.x(),l.y()),i=new A.math.Line(x,i),c=new A.math.Point2D(u,c),d=new A.math.Point2D(g,d),S=A.math.getPerpendicularLine(i,c,10),m=A.math.getPerpendicularLine(i,d,10);r.hitFunc(function(e){e.beginPath(),e.moveTo(S.getBegin().getX(),S.getBegin().getY()),e.lineTo(S.getEnd().getX(),S.getEnd().getY()),e.lineTo(m.getEnd().getX(),m.getEnd().getY()),e.lineTo(m.getBegin().getX(),m.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});d=new A.math.Point2D(i.getBegin().getX(),i.getBegin().getY()-10),r=new A.math.Line(i.getBegin(),d),d=A.math.getAngle(i,r),r=d*Math.PI/180;o.x(i.getBegin().getX()+o.radius()*Math.sin(r)),o.y(i.getBegin().getY()+o.radius()*Math.cos(r)),o.rotation(-d);r=a.getText();r.setText(r.meta.textExpr);o=i.getBegin().getX()>i.getEnd().getX()?0:-1,d=i.getBegin().getY()>i.getEnd().getY()?-1:0,t={x:i.getEnd().getX()+o*r.width(),y:i.getEnd().getY()+d*t.applyZoomScale(15).y};a.position(t)},(A=A||{}).tool=A.tool||{},A.tool.draw=A.tool.draw||{};y=y||{};A.tool.draw.defaultCircleLabelText="{surface}",A.tool.draw.CircleFactory=function(){this.getGroupName=function(){return"circle-group"},this.getNPoints=function(){return 2},this.getTimeout=function(){return 0}},A.tool.draw.CircleFactory.prototype.isFactoryGroup=function(e){return this.getGroupName()===e.name()},A.tool.draw.CircleFactory.prototype.create=function(e,t,n){var i=Math.abs(e[0].getX()-e[1].getX()),r=Math.abs(e[0].getY()-e[1].getY()),o=Math.round(Math.sqrt(i*i+r*r)),i=new A.math.Circle(e[0],o),r=new y.Circle({x:i.getCenter().getX(),y:i.getCenter().getY(),radius:i.getRadius(),stroke:t.getLineColour(),strokeWidth:t.getStrokeWidth(),strokeScaleEnabled:!1,name:"shape"}),e=new y.Text({fontSize:t.getFontSize(),fontFamily:t.getFontFamily(),fill:t.getLineColour(),padding:t.getTextPadding(),shadowColor:t.getShadowLineColour(),shadowOffset:t.getShadowOffset(),name:"text"}),o="",o=void 0!==A.tool.draw.circleLabelText?A.tool.draw.circleLabelText:A.tool.draw.defaultCircleLabelText,n=i.quantify(n,A.utils.getFlags(o));e.setText(A.utils.replaceFlags(o,n)),e.meta={textExpr:o,quantification:n};var a,o=new y.Label({x:i.getCenter().getX(),y:i.getCenter().getY(),scale:t.applyZoomScale(1),visible:0!==o.length,name:"label"});o.add(e),o.add(new y.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()})),A.tool.draw.debug&&(a=A.tool.draw.getShadowCircle(i));i=new y.Group;return i.name(this.getGroupName()),a&&i.add(a),i.add(o),i.add(r),i.visible(!0),i},A.tool.draw.CircleFactory.prototype.getAnchors=function(e,t){var n=e.x(),i=e.y(),r=e.radius(),e=[];return e.push(A.tool.draw.getDefaultAnchor(n-r,i,"left",t)),e.push(A.tool.draw.getDefaultAnchor(n+r,i,"right",t)),e.push(A.tool.draw.getDefaultAnchor(n,i-r,"bottom",t)),e.push(A.tool.draw.getDefaultAnchor(n,i+r,"top",t)),e},A.tool.draw.CircleFactory.prototype.update=function(e,t,n){var i,r=e.getParent(),o=r.getChildren(function(e){return"shape"===e.name()})[0],a=r.getChildren(function(e){return"label"===e.name()})[0],s=r.getChildren(function(e){return"left"===e.id()})[0],l=r.getChildren(function(e){return"right"===e.id()})[0],u=r.getChildren(function(e){return"bottom"===e.id()})[0],c=r.getChildren(function(e){return"top"===e.id()})[0];A.tool.draw.debug&&(i=r.getChildren(function(e){return"shadow"===e.name()})[0]);var g,d={x:o.x(),y:o.y()};switch(e.id()){case"left":g=d.x-e.x(),s.y(l.y()),l.x(d.x+g),u.y(d.y-g),c.y(d.y+g);break;case"right":g=e.x()-d.x,l.y(s.y()),s.x(d.x-g),u.y(d.y-g),c.y(d.y+g);break;case"bottom":g=d.y-e.y(),u.x(c.x()),s.x(d.x-g),l.x(d.x+g),c.y(d.y+g);break;case"top":g=e.y()-d.y,c.x(u.x()),s.x(d.x-g),l.x(d.x+g),u.y(d.y-g);break;default:A.logger.error("Unhandled anchor id: "+e.id())}o.radius(Math.abs(g));o=new A.math.Point2D(r.x()+d.x,r.y()+d.y),o=new A.math.Circle(o,g);i&&(i.destroy(),r.add(A.tool.draw.getShadowCircle(o,r))),a.position({x:d.x,y:d.y}),A.tool.draw.updateCircleQuantification(r,n)},A.tool.draw.CircleFactory.prototype.updateQuantification=function(e,t){A.tool.draw.updateCircleQuantification(e,t)},A.tool.draw.updateCircleQuantification=function(e,t){var n=e.getChildren(function(e){return"shape"===e.name()})[0],i=e.getChildren(function(e){return"label"===e.name()})[0],e=new A.math.Point2D(e.x()+n.x(),e.y()+n.y()),n=new A.math.Circle(e,n.radius()),i=i.getText(),t=n.quantify(t,A.utils.getFlags(i.meta.textExpr));i.setText(A.utils.replaceFlags(i.meta.textExpr,t)),i.meta.quantification=t},A.tool.draw.getShadowCircle=function(e,t){var n=0,i=0;void 0!==t&&(n=t.x(),i=t.y());var r=new y.Group;r.name("shadow");for(var o=e.getRound(),a=0;an.x&&(e.x(n.x),i=!0),e.y()n.y&&(e.y(n.y),i=!0),i},A.tool.validateGroupPosition=function(e,t){var n=t.getChildren(A.draw.isNodeNameShape)[0],i=A.tool.getAnchorMin(t);if(void 0===i)return null;var r={x:-i.x,y:-i.y},n={x:e.x-(i.x+Math.abs(n.width())),y:e.y-(i.y+Math.abs(n.height()))};return A.tool.boundNodePosition(t,r,n)},A.tool.validateAnchorPosition=function(e,t){var n=t.getParent(),i={x:-n.x(),y:-n.y()},n={x:e.x-n.x(),y:e.y-n.y()};return A.tool.boundNodePosition(t,i,n)},(A=A||{}).tool=A.tool||{};y=y||{};A.tool.GetShapeDisplayName=function(e){var t="shape";return e instanceof y.Line?t=4===e.points().length?"line":6===e.points().length?"protractor":"roi":e instanceof y.Rect?t="rectangle":e instanceof y.Ellipse&&(t="ellipse"),t},A.tool.DrawGroupCommand=function(e,t,n,i){var r=void 0!==i&&i,o=e.getParent();this.getName=function(){return"Draw-"+t},this.execute=function(){o.add(e),n.draw(),r||this.onExecute({type:"drawcreate",id:e.id()})},this.undo=function(){e.remove(),n.draw(),this.onUndo({type:"drawdelete",id:e.id()})}},A.tool.DrawGroupCommand.prototype.onExecute=function(e){},A.tool.DrawGroupCommand.prototype.onUndo=function(e){},A.tool.MoveGroupCommand=function(t,e,n,i){this.getName=function(){return"Move-"+e},this.execute=function(){t.move(n),i.draw(),this.onExecute({type:"drawmove",id:t.id()})},this.undo=function(){var e={x:-n.x,y:-n.y};t.move(e),i.draw(),this.onUndo({type:"drawmove",id:t.id()})}},A.tool.MoveGroupCommand.prototype.onExecute=function(e){},A.tool.MoveGroupCommand.prototype.onUndo=function(e){},A.tool.ChangeGroupCommand=function(e,t,n,i,r,o,a){this.getName=function(){return"Change-"+e},this.execute=function(){t(i,a,o),r.draw(),this.onExecute({type:"drawchange"})},this.undo=function(){t(n,a,o),r.draw(),this.onUndo({type:"drawchange"})}},A.tool.ChangeGroupCommand.prototype.onExecute=function(e){},A.tool.ChangeGroupCommand.prototype.onUndo=function(e){},A.tool.DeleteGroupCommand=function(e,t,n){var i=e.getParent();this.getName=function(){return"Delete-"+t},this.execute=function(){e.remove(),n.draw(),this.onExecute({type:"drawdelete",id:e.id()})},this.undo=function(){i.add(e),n.draw(),this.onUndo({type:"drawcreate",id:e.id()})}},A.tool.DeleteGroupCommand.prototype.onExecute=function(e){},A.tool.DeleteGroupCommand.prototype.onUndo=function(e){},(A=A||{}).tool=A.tool||{},A.tool.draw=A.tool.draw||{};y=y||{};A.tool.draw.getDefaultAnchor=function(e,t,n,i){return new y.Ellipse({x:e,y:t,stroke:"#999",fill:"rgba(100,100,100,0.7",strokeWidth:i.getStrokeWidth(),strokeScaleEnabled:!1,radius:i.applyZoomScale(3),name:"anchor",id:n,dragOnTop:!1,draggable:!0,visible:!1})},A.tool.ShapeEditor=function(o){var a=null,s=null,l=null,u=null,e=!1,c=null;function n(e){l&&l.getParent()&&l.getParent().find(".anchor").forEach(e)}function t(t){n(function(e){e.visible(t)})}function g(){n(function(e){e.remove()})}function d(){if(l&&l.getLayer())for(var e=l.getParent(),t=s.getAnchors(l,o.getStyle()),n=0;ni.getEnd().getX()?0:-1,o=i.getBegin().getY()>i.getEnd().getY()?-1:0,e=new y.Label({x:i.getEnd().getX()+n*c.width(),y:i.getEnd().getY()+o*t.applyZoomScale(15).y,scale:t.applyZoomScale(1),visible:0!==e.length,name:"label"});e.add(c),e.add(new y.Tag({fill:t.getLineColour(),opacity:t.getTagOpacity()}));t=new y.Group;return t.name(this.getGroupName()),t.add(e),t.add(s),t.add(u),t.add(r),t.visible(!0),t},A.tool.draw.RulerFactory.prototype.getAnchors=function(e,t){var n=e.points(),i=[];return i.push(A.tool.draw.getDefaultAnchor(n[0]+e.x(),n[1]+e.y(),"begin",t)),i.push(A.tool.draw.getDefaultAnchor(n[2]+e.x(),n[3]+e.y(),"end",t)),i},A.tool.draw.RulerFactory.prototype.update=function(e,t,n){var i=e.getParent(),r=i.getChildren(function(e){return"shape"===e.name()})[0],o=i.getChildren(function(e){return"shape-tick0"===e.name()})[0],a=i.getChildren(function(e){return"shape-tick1"===e.name()})[0],s=i.getChildren(function(e){return"label"===e.name()})[0],l=i.getChildren(function(e){return"begin"===e.id()})[0],u=i.getChildren(function(e){return"end"===e.id()})[0];switch(e.id()){case"begin":l.x(e.x()),l.y(e.y());break;case"end":u.x(e.x()),u.y(e.y())}var c=l.x()-r.x(),g=l.y()-r.y(),d=u.x()-r.x(),x=u.y()-r.y();r.points([c,g,d,x]);var S=new A.math.Point2D(l.x(),l.y()),i=new A.math.Point2D(u.x(),u.y()),i=new A.math.Line(S,i),g=new A.math.Point2D(c,g),x=new A.math.Point2D(d,x),m=A.math.getPerpendicularLine(i,g,t.scale(10));o.points([m.getBegin().getX(),m.getBegin().getY(),m.getEnd().getX(),m.getEnd().getY()]);var h=A.math.getPerpendicularLine(i,x,t.scale(10));a.points([h.getBegin().getX(),h.getBegin().getY(),h.getEnd().getX(),h.getEnd().getY()]),r.hitFunc(function(e){e.beginPath(),e.moveTo(m.getBegin().getX(),m.getBegin().getY()),e.lineTo(m.getEnd().getX(),m.getEnd().getY()),e.lineTo(h.getEnd().getX(),h.getEnd().getY()),e.lineTo(h.getBegin().getX(),h.getBegin().getY()),e.closePath(),e.fillStrokeShape(this)});a=s.getText(),r=i.quantify(n,A.utils.getFlags(a.meta.textExpr));a.setText(A.utils.replaceFlags(a.meta.textExpr,r)),a.meta.quantification=r;n=i.getBegin().getX()>i.getEnd().getX()?0:-1,r=i.getBegin().getY()>i.getEnd().getY()?-1:0,t={x:i.getEnd().getX()+n*a.width(),y:i.getEnd().getY()+r*t.applyZoomScale(15).y};s.position(t)},(A=A||{}).tool=A.tool||{},A.tool.Scroll=function(a){var s=this;this.started=!1;var t=null;this.mousedown=function(e){var t=A.gui.getLayerDetailsFromEvent(e),n=a.getLayerGroupById(t.groupId).getActiveViewLayer(),t=n.getViewController();t.isPlaying()&&t.stop(),s.started=!0,s.x0=e._x,s.y0=e._y;e=n.displayToPlanePos(e._x,e._y);t.setCurrentPosition2D(e.x,e.y)},this.mousemove=function(e){var t,n,i,r,o;s.started&&(r=A.gui.getLayerDetailsFromEvent(e),t=a.getLayerGroupById(r.groupId).getActiveViewLayer().getViewController(),o=e._y-s.y0,(n=15>>0;if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var i=arguments[1],r=0;r1: Certification refers to official medical software certification that are issued by the FDA or EU Notified Bodies. The sentence here serves as a reminder that the Dicom Web Viewer is not ceritifed, and comes with no warranties (and no possible liability of its authors) as stated in the [license](license.txt). To learn more about standards used in certification, see the [wikipedia Medical software](https://en.wikipedia.org/wiki/Medical_software) page. +1: Certification refers to official medical software certification that are issued by the FDA or EU Notified Bodies. The sentence here serves as a reminder that the Dicom Web Viewer is not ceritifed, and comes with no warranties (and no possible liability of its authors) as stated in the [license](license.txt). To learn more about standards used in certification, see the [wikipedia Medical software](https://en.wikipedia.org/wiki/Medical_software) page. diff --git a/resources/doc/img/architecture-overview.png b/resources/doc/img/architecture-overview.png deleted file mode 100644 index c1421e966c..0000000000 Binary files a/resources/doc/img/architecture-overview.png and /dev/null differ diff --git a/resources/doc/img/classes-io.png b/resources/doc/img/classes-io.png new file mode 100644 index 0000000000..b7cdb927dd Binary files /dev/null and b/resources/doc/img/classes-io.png differ diff --git a/resources/doc/img/classes-io.puml b/resources/doc/img/classes-io.puml new file mode 100644 index 0000000000..07d3f2965e --- /dev/null +++ b/resources/doc/img/classes-io.puml @@ -0,0 +1,32 @@ +@startuml + +class XMLHttpRequest #lightblue;line:green +class FileReader #lightblue;line:green + +class App { + loadURLs() + loadFiles() + loadImageObject() +} +class LoadController +class UrlsLoader +class FilesLoader +class MemoryLoader +interface SpecificDataLoader ##[dashed] { + canLoadUrl() + canLoadFile() +} + +App ..> LoadController: uses +LoadController ..> UrlsLoader: uses +LoadController ..> FilesLoader: uses +LoadController ..> MemoryLoader: uses + +UrlsLoader .up.> XMLHttpRequest: uses +FilesLoader .up.> FileReader: uses + +UrlsLoader ..> SpecificDataLoader: uses +FilesLoader ..> SpecificDataLoader: uses +MemoryLoader ..> SpecificDataLoader: uses + +@enduml diff --git a/resources/doc/img/classes-layers-draw.png b/resources/doc/img/classes-layers-draw.png new file mode 100644 index 0000000000..ea07353602 Binary files /dev/null and b/resources/doc/img/classes-layers-draw.png differ diff --git a/resources/doc/img/classes-layers-draw.puml b/resources/doc/img/classes-layers-draw.puml new file mode 100644 index 0000000000..e56b6fb0f7 --- /dev/null +++ b/resources/doc/img/classes-layers-draw.puml @@ -0,0 +1,18 @@ +@startuml + +class KonvaShape { + (mvc:model) +} +class DrawLayer { + (mvc:view) +} +class DrawController + +circle User + +KonvaShape --> DrawLayer: updates +DrawLayer -- User: sees +User -up-> DrawController: uses +DrawController -up-> KonvaShape: manipulates + +@enduml diff --git a/resources/doc/img/classes-layers-view.png b/resources/doc/img/classes-layers-view.png new file mode 100644 index 0000000000..2a77e55598 Binary files /dev/null and b/resources/doc/img/classes-layers-view.png differ diff --git a/resources/doc/img/classes-layers-view.puml b/resources/doc/img/classes-layers-view.puml new file mode 100644 index 0000000000..f96edb07ef --- /dev/null +++ b/resources/doc/img/classes-layers-view.puml @@ -0,0 +1,19 @@ +@startuml + +class View { + (mvc:model) +} +class ViewLayer { + (mvc:view) +} +class ViewController + +circle User + +View --> ViewLayer: updates +ViewLayer -- User: sees +User -up-> ViewController: uses +ViewController -up-> View: manipulates + + +@enduml diff --git a/resources/doc/img/classes-layers.png b/resources/doc/img/classes-layers.png new file mode 100644 index 0000000000..f4e54134c4 Binary files /dev/null and b/resources/doc/img/classes-layers.png differ diff --git a/resources/doc/img/classes-layers.puml b/resources/doc/img/classes-layers.puml new file mode 100644 index 0000000000..1a0bd68d2d --- /dev/null +++ b/resources/doc/img/classes-layers.puml @@ -0,0 +1,19 @@ +@startuml + +class App { + getLayerGroupById() +} +class Stage +class LayerGroup { + getActiveViewLayer() + getActiveDrawLayer() +} +class ViewLayer +class DrawLayer + +App --* "1" Stage +Stage --* "0..*" LayerGroup +LayerGroup --* "0..*" ViewLayer +LayerGroup --* "0..*" DrawLayer + +@enduml diff --git a/resources/doc/img/readme.md b/resources/doc/img/readme.md new file mode 100644 index 0000000000..3837ba6a9d --- /dev/null +++ b/resources/doc/img/readme.md @@ -0,0 +1,7 @@ +# Architecture diagrams + +Made using [PlantUML](https://plantuml.com/). + +Images are generated with [planttext](https://www.planttext.com/). + +nice page on class relations: [uml-class-diagram-relationships.html](http://usna86-techbits.blogspot.com/2012/11/uml-class-diagram-relationships.html) diff --git a/resources/doc/img/sequence-view-creation.png b/resources/doc/img/sequence-view-creation.png new file mode 100644 index 0000000000..087942f684 Binary files /dev/null and b/resources/doc/img/sequence-view-creation.png differ diff --git a/resources/doc/img/sequence-view-creation.puml b/resources/doc/img/sequence-view-creation.puml new file mode 100644 index 0000000000..51abadd67c --- /dev/null +++ b/resources/doc/img/sequence-view-creation.puml @@ -0,0 +1,22 @@ +@startuml + +View -> Image: get image data +activate Image +Image -> View: image iterator +deactivate Image + +View -> WindowLut: get window lut +activate WindowLut +WindowLut --> RescaleLut: get rescale lut +activate RescaleLut +RescaleLut --> WindowLut: rescale lut +deactivate RescaleLut +WindowLut -> View: window lut +deactivate WindowLut + +View -> ColourMap: get colour map +activate ColourMap +ColourMap -> View: map +deactivate ColourMap + +@enduml diff --git a/resources/doc/tutorials/architecture.md b/resources/doc/tutorials/architecture.md index 4e8d492ccc..507aecfb73 100644 --- a/resources/doc/tutorials/architecture.md +++ b/resources/doc/tutorials/architecture.md @@ -1,15 +1,58 @@ This page lists details about the dwv architecture. -## Overview -![archi](architecture-overview.png) - -- `App`: main class -- `LoadController`: handles I/O, the first layer handles the source being - `File`, `Url`, `Memory` and the second one handles data type, `Dicom`, `Zip` - `rawImage`, `rawVideo` and `json` -- `LayerController`: the layers follow a - [model-view-controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) - (MVC) design for the different kinds of layers, -for now `View` and `Draw`. -- `ToolboxController`: handles tools and dispatches interaction events to the selected - one +* [Layers](#layers) +* [Data load](#data-load) +* [View creation](#view-creation) + +## Layers +![classes-layers](classes-layers.png) + +The first level is the stage, this class handles a list of LayerGroups for optional synchronisation. A layerGroup is +a group of layers associated to an HTML element, for now of type `View` and `Draw`. The configuration of the stage +is done at the creation of the app. See [app::init](./dwv.App.html#init) method for details. Each layer class will +create its own HTML div with an id created by [dwv::gui::getLayerGroupDivId](./dwv.gui.html#.getLayerGroupDivId). Layers +will typically contain a HTML canvas to display its content. Use the [dwv::gui::getLayerDetailsFromEvent](./dwv.gui.html#.getLayerDetailsFromEvent) method to extract the layer details from an event generated from a layer canvas. +You can then access the layer object via the app `getLayerGroupById` method. + +![classes-layers-view](classes-layers-view.png) + +The View class contains a 2D view of the image that could be 3D + t. Layers follow the [model-view-controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (MVC) design. In the view case, the model is the View, the view the ViewLayer and the controller the ViewController. + +![classes-layers-draw](classes-layers-draw.png) + +In the case of the draw, the model is the KonvaShape, the view is the DrawLayer and the controller is the DrawController. +The shape will use the ViewController for quantification when it needs to access the underlying pixel values. + +## Data load +![classes-io](classes-io.png) + +Data can come from 3 types of source: url (via a [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)), file (via a [FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)) or directly as a memory buffer. The 3 'meta' loaders responsible for each source are: [UrlsLoader](./dwv.io.UrlsLoader.html), [FilesLoader](./dwv.io.FilesLoader.html) and [MemoryLoader](./dwv.io.MemoryLoader.html). + +Each 'meta' loader will then delegate the individual data load to a specialised loader according +to its answer to the `canLoadUrl` or `canLoadFile` call. The current specialised loaders are: +1. [DicomDataLoader](./dwv.io.DicomDataLoader.html): reads DICOM dataIndex +1. [JSONTextLoader](./dwv.io.JSONTextLoader.html): for JSON data +1. [RawImageLoader](./dwv.io.RawImageLoader.html): for image formats supported by the browser +1. [RawVideoLoader](./dwv.io.RawVideoLoader.html): for video formats supported by the browser +1. [ZipLoader](./dwv.io.ZipLoader.html): for data compressed in a ZIP file + +## View creation +![sequence-view-creation](sequence-view-creation.png) + +The library will use a series of steps and LookUp Tables (LUT) to convert the file data into the +canvas array data: +1. Extract the data from the recreated 3D volume using position and orientation + and abstracted folowing the [iterator pattern](https://en.wikipedia.org/wiki/Iterator_pattern) + (see [image/iterator.js](./dwv.image.html#.range)) +1. From stored type range to physical range using a [Modality LUT](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.html): rescale slope and intercept are used + in the conversion equation: `y = slope * x + intercept` + (see [image/rescaleLut.js](./dwv.image.RescaleLut.html)) +1. Select part of the range using a [VOI LUT](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.11.2.html#table_C.11-2) (Value Of Interest): window width and level (or centre) + allow to focus on a specific range (especially useful for normed data such + as in CT) + (see [image/windowLut.js](./dwv.image.WindowLut.html)) +1. Assign a colour to each values using a colour map + (see [image/lut.js](./dwv.image.lut.html)) +1. You now have the canvas data! + +All this is materialised in the `dwv.image.generateImageData*` functions. diff --git a/resources/doc/tutorials/errors.md b/resources/doc/tutorials/errors.md index 7d7ca3945c..c22aefe8db 100644 --- a/resources/doc/tutorials/errors.md +++ b/resources/doc/tutorials/errors.md @@ -1,12 +1,12 @@ This page lists the possible error thrown by the application with some explanation and a possible quick fix. -## Not a valid DICOM file +## Not a valid DICOM file Message: `Not a valid DICOM file (no magic DICM word found)` Context: Loading a DICOM file. -All DICOM files should start with the DICOM prefix `DICM` (see [tutorial-conformance.html#validity](./tutorial-conformance.html#validity) for some exceptions and more details). +All DICOM files should start with the DICOM prefix `DICM` (see [tutorial-conformance.html#validity](./tutorial-conformance.html#validity) for some exceptions and more details). To fix the data you can use tolerant conversion tools such as [gdcm](http://gdcm.sourceforge.net/wiki/index.php/Main_Page). Convert the data to raw with `gdcmconv --raw -i {in-dcm_file_path} -o {out-dcm_file_path}`. @@ -16,7 +16,7 @@ Message: `RequestError: An error occurred while reading 'file.dcm' (http status: Context: Loading a DICOM file. -The data cannot be accessed, either because it does not exist, there has been a problem while transmitting it or because you do not have access permission to it. +The data cannot be accessed, either because it does not exist, there has been a problem while transmitting it or because you do not have access permission to it. The debug window of your browser should give you more info of the reason why the request did not succeed. diff --git a/resources/doc/tutorials/integrations.md b/resources/doc/tutorials/integrations.md index c282487193..f11b6802e6 100644 --- a/resources/doc/tutorials/integrations.md +++ b/resources/doc/tutorials/integrations.md @@ -2,7 +2,7 @@ This page details some integrations of dwv. PACS integrations will use Web Acces Quick summary: [Conquest](#conquest) ✅, [dcm4chee](#dcm4chee) ✅, [Orthanc](#orthanc) ✅, [ClearCanvas](#clearcanvas) ❌, [Google](#google) ✅, [WordPress](#wordpress) ✅ -## Conquest +## Conquest [Conquest](http://ingenium.home.xs4all.nl/dicom.html): _"a full featured DICOM server based on the public domain UCDMC DICOM code"_ ([entry](http://www.idoimaging.com/program/183) on idoimaging). License: Public Domain (see [medfloss](http://www.medfloss.org/node/93)). Tested version: [1.4.17](http://forum.image-systems.biz/viewtopic.php?f=33&t=18892). See this [thread](http://85.214.110.44/forum/forum/index.php?thread/17196-conquest-and-html5-js-dicom-viewer-dwv-dwv016-below/) on the Conquest forum and the issue [#15](https://github.com/ivmartel/dwv/issues/15). Operational since dwv `v0.3.0`, available in the [dwv-jqmobile](https://github.com/ivmartel/dwv-jqmobile), [dwv-jqui](https://github.com/ivmartel/dwv-jqui) and [dwv-simplistic](https://github.com/ivmartel/dwv-simplistic) demos. @@ -12,9 +12,9 @@ To setup DWV, follow the instructions written in the lua files of the respective Conquest installation details: * Under Fedora 18: [install under ubuntu linux](http://blog.kyodium.net/2010/10/install-conquest-on-ubuntu-1004.html) after [installing apache](http://www.howtoforge.com/installing-apache2-with-php5-and-mysql-support-on-fedora-17-lamp) * the g++ fedora package is called `gcc-c++` - * the cgi-bin folder is in `/var/www` + * the cgi-bin folder is in `/var/www` * launching services is done using `systemctl`: for example `systemctl start httpd.service` and `systemctl start mysqld.service` - * Under Windows7: + * Under Windows7: * using [wamp5](http://www.wampserver.com/) (install as admin, see [forum](http://forum.wampserver.com/read.php?1,88043)), the cgi bin folder is in `wamp/bin/apache/apache##/cgi-bin` * to launch the PACS: start the wamp service and run the `ConquestDICOMServer.exe`, you can then access the web interface at http://127.0.0.1/cgi-bin/dgate.exe?mode=top. diff --git a/resources/doc/tutorials/module.md b/resources/doc/tutorials/module.md index 0acf5a43f9..be6123b21d 100644 --- a/resources/doc/tutorials/module.md +++ b/resources/doc/tutorials/module.md @@ -31,7 +31,7 @@ To execute it, run: node main.js ``` -You can create a test packages by running `npm pack` in a folder containing a `package.json` file. Install it +You can create a test packages by running `npm pack` in a folder containing a `package.json` file. Install it by running `npm install my-package.tgz`. Note: I've not tested all the classes of dwv in node, it is possible some of them use browser provided methods that node does not have. For example dwv.App uses the HTML 'canvas' which can be added by installing a 'node-canvas' (see [help](https://github.com/konvajs/konva#5-nodejs) from Konva). Konva is disabled in the module `intro.js` file. Another one could be the 'XMLHttpRequest'... diff --git a/resources/doc/tutorials/standards.md b/resources/doc/tutorials/standards.md index 19af738b9b..95716dc924 100644 --- a/resources/doc/tutorials/standards.md +++ b/resources/doc/tutorials/standards.md @@ -9,7 +9,7 @@ These are the standards that should be used when coding for this project. * Test: use of [QUnit](https://qunitjs.com/) via [Karma](https://karma-runner.github.io), * Documentation: use of [jsdoc](https://jsdoc.app/), * Versioning: [Semantic Versioning](http://semver.org/) - * Branch: try to follow some kind of [branching model](http://nvie.com/posts/a-successful-git-branching-model/) + * Branch: try to follow some kind of [branching model](http://nvie.com/posts/a-successful-git-branching-model/) These standards are enforced using Continuous Integration with [github-actions](https://github.com/features/actions): builds using [node](http://nodejs.org/) (see `.github/workflows/nodejs-ci.yml`) and [yarn](https://classic.yarnpkg.com). The CI basically executes `yarn install` that reads the `package.json` file and then runs `yarn run test`. This test target is configured to run a task runner called [Grunt](http://gruntjs.com/) which is configured with the `Gruntfile.js` file. The `package.json` file contains shortcuts to grunt scripts: * `yarn run test` -> [grunt-karma](https://www.npmjs.org/package/grunt-karma) that allows to run qunit tests using a headless browser such a Google Chrome @@ -23,7 +23,6 @@ Others * Github ([status](https://status.github.com/)), [build status](https://github.com/ivmartel/dwv/actions) * [Code Climate](https://codeclimate.com) ([status](http://status.codeclimate.com/)): code review + test coverage + lint, [dwv page](https://codeclimate.com/github/ivmartel/dwv) * [David-dm](https://david-dm.org/): dependency up to date checker, [dwv page](https://david-dm.org/ivmartel/dwv) - * [Coveralls](https://coveralls.io/) ([status](http://status.coveralls.io/)): test coverage, [dwv page](https://coveralls.io/github/ivmartel/dwv) * [Transifex](https://www.transifex.com): translations, [dwv page](https://www.transifex.com/ivmartel/dwv/) * [Dependabot](https://github.com/dependabot): dependency up to date bot checker diff --git a/resources/doc/tutorials/user-stories.md b/resources/doc/tutorials/user-stories.md index 5de07eeedd..ee576c8796 100644 --- a/resources/doc/tutorials/user-stories.md +++ b/resources/doc/tutorials/user-stories.md @@ -6,7 +6,7 @@ Definitions: * _DICOM data_: a single or multiple DICOM files. * _Image data_: a single or multiple image files in JPG or PNG format. * _File_: represents file data typically obtained from the underlying file system. It is provided via an HTML input field with the 'file' type or via drag and drop. See the HTML5 [FileAPI](https://www.w3.org/TR/FileAPI/) for more details. -* _Url_: the data is accessed via a url which resolves to data published on the same server as the application or on one that allows Cross-Origin Resource Sharing (CORS). +* _Url_: the data is accessed via a url which resolves to data published on the same server as the application or on one that allows Cross-Origin Resource Sharing (CORS). #### DWV-URS-IO-001 Load DICOM file(s) The user can load DICOM data provided as one or multiple HTML File(s). @@ -66,10 +66,10 @@ Only the first series of the first study of the first patient will be loaded. The library can be integrated in a web application, a browser without any extensions must be sufficient to run the application. #### DWV-URS-UI-002 Display -The user can generate a view of the image data in an optimised way according to external window properties. +The user can generate a view of the image data in an optimised way according to external window properties. #### DWV-URS-UI-003 Interaction -The user can interact with content with a computer mouse or with a finger on touch enabled devices. +The user can interact with content with a computer mouse or with a finger on touch enabled devices. #### DWV-URS-UI-004 Overlay information The user can generate an overlay to view the current window and level, the zoom level, the X and Y coordinates and the pixel intensity. @@ -91,7 +91,7 @@ The user can change the window/level of the displayed data. The window/level val Specific window/level data pre-sets must be made available. They are modality specific. For examples see: [radiantviewer](http://www.radiantviewer.com/dicom-viewer-manual/change_brightness_contrast.htm) or this [thread](http://forum.dicom-cd.de/viewtopic.php?p=9998&sid=28bfed23e680aae327c66d5ab7d28396). #### DWV-URS-UI-009 Slice scroll -The user can scroll the different slices of a multi-slice data using `left click drag` or `one touch drag`. +The user can scroll the different slices of a multi-slice data using `left click drag` or `one touch drag`. #### DWV-URS-UI-010 Zoom/Pan The user can zoom/pan the displayed data. The zoom value must be displayed to the user. The user must be able to reset the zoom/pan value to their original values. The default interaction are: @@ -135,7 +135,7 @@ Examples: The DICOM data could be uploaded to a private cloud storage with the patient owning it. The storage could be a generic one (for example Google Drive) or a specific medical one (for example a hospital HIS with external access). A storage system that wants to meet the previous requirements would need to implement the following: * a DICOM Web Viewer to view data * a DICOM Web Annotator to add annotations to data - * a DICOM Web Anonymiser to view/download anonymous data + * a DICOM Web Anonymiser to view/download anonymous data Each functionality being accessible via access rights such as view, annotate and download decided at the moment of sharing the data. View and annotate could be done live between multiple users. Download can only be done anonymously to avoid a data breach. diff --git a/resources/scripts/finish-release.sh b/resources/scripts/finish-release.sh index e52e2c5b28..bc594bdabb 100755 --- a/resources/scripts/finish-release.sh +++ b/resources/scripts/finish-release.sh @@ -55,12 +55,14 @@ fi info "Finishing release for '$releaseVersion' with next version '$nextVersion'..." +# branch name +releaseBranch="v${releaseVersion}" + ################### if [ $step -eq 1 ] then info "(1/4) commit prepared changes" - releaseBranch="v${releaseVersion}" git commit -a -m "Release ${releaseBranch}" ((step++)) diff --git a/resources/scripts/prep-release.sh b/resources/scripts/prep-release.sh index 413a8fa1e3..492d217cf1 100755 --- a/resources/scripts/prep-release.sh +++ b/resources/scripts/prep-release.sh @@ -55,6 +55,9 @@ fi info "Preparing release for '$releaseVersion' with previous version '$prevVersion'..." +# branch name +releaseBranch="v${releaseVersion}" + ################### if [ $step -eq 1 ] then @@ -62,7 +65,6 @@ then git checkout develop git pull - releaseBranch="v${releaseVersion}" git checkout -b $releaseBranch ((step++)) diff --git a/src/app/application.js b/src/app/application.js index d42f54d008..82c4047f02 100644 --- a/src/app/application.js +++ b/src/app/application.js @@ -20,14 +20,11 @@ dwv.App = function () { // toolbox controller var toolboxController = null; - // layer controller - var layerController = null; - // load controller var loadController = null; - // first load item flag - var isFirstLoadItem = null; + // stage + var stage = null; // UndoStack var undoStack = null; @@ -46,54 +43,66 @@ dwv.App = function () { /** * Get the image. * + * @param {number} index The data index. * @returns {Image} The associated image. */ - this.getImage = function () { - return dataController.get(0).image; + this.getImage = function (index) { + return dataController.get(index).image; + }; + /** + * Get the last loaded image. + * + * @returns {Image} The image. + */ + this.getLastImage = function () { + return dataController.get(dataController.length() - 1).image; }; /** * Set the image. * + * @param {number} index The data index. * @param {Image} img The associated image. */ - this.setImage = function (img) { - dataController.setImage(img, 0); + this.setImage = function (index, img) { + dataController.setImage(index, img); }; - /** - * Get the meta data. + * Set the last image. * - * @returns {object} The list of meta data. + * @param {Image} img The associated image. */ - this.getMetaData = function () { - return dataController.get(0).meta; + this.setLastImage = function (img) { + dataController.setImage(dataController.length() - 1, img); }; /** - * Is the data mono-slice? + * Get the meta data. * - * @returns {boolean} True if the data only contains one slice. + * @param {number} index The data index. + * @returns {object} The list of meta data. */ - this.isMonoSliceData = function () { - return loadController.isMonoSliceData(); + this.getMetaData = function (index) { + return dataController.get(index).meta; }; + /** - * Is the data mono-frame? + * Get the number of loaded data. * - * @returns {boolean} True if the data only contains one frame. + * @returns {number} The number. */ - this.isMonoFrameData = function () { - var viewLayer = layerController.getActiveViewLayer(); - var controller = viewLayer.getViewController(); - return controller.isMonoFrameData(); + this.getNumberOfLoadedData = function () { + return dataController.length(); }; + /** * Can the data be scrolled? * - * @returns {boolean} True if the data has more than one slice or frame. + * @returns {boolean} True if the data has a third dimension greater than one. */ this.canScroll = function () { - return !this.isMonoSliceData() || !this.isMonoFrameData(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); + var controller = viewLayer.getViewController(); + return controller.canScroll(); }; /** @@ -102,7 +111,7 @@ dwv.App = function () { * @returns {boolean} True if the data is monochrome. */ this.canWindowLevel = function () { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); var controller = viewLayer.getViewController(); return controller.canWindowLevel(); }; @@ -113,7 +122,7 @@ dwv.App = function () { * @returns {object} The scale as {x,y}. */ this.getAddedScale = function () { - return layerController.getAddedScale(); + return stage.getActiveLayerGroup().getAddedScale(); }; /** @@ -122,7 +131,7 @@ dwv.App = function () { * @returns {object} The scale as {x,y}. */ this.getBaseScale = function () { - return layerController.getBaseScale(); + return stage.getActiveLayerGroup().getBaseScale(); }; /** @@ -131,7 +140,7 @@ dwv.App = function () { * @returns {object} The offset. */ this.getOffset = function () { - return layerController.getOffset(); + return stage.getActiveLayerGroup().getOffset(); }; /** @@ -144,13 +153,44 @@ dwv.App = function () { }; /** - * Get the layer controller. - * The controller is available after the first loaded item. + * Get the active layer group. + * The layer is available after the first loaded item. * - * @returns {object} The controller. + * @returns {dwv.gui.LayerGroup} The layer group. */ - this.getLayerController = function () { - return layerController; + this.getActiveLayerGroup = function () { + return stage.getActiveLayerGroup(); + }; + + /** + * Get the view layers associated to a data index. + * The layer are available after the first loaded item. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getViewLayersByDataIndex = function (index) { + return stage.getViewLayersByDataIndex(index); + }; + + /** + * Get a layer group by id. + * The layer is available after the first loaded item. + * + * @param {number} groupId The group id. + * @returns {dwv.gui.LayerGroup} The layer group. + */ + this.getLayerGroupById = function (groupId) { + return stage.getLayerGroup(groupId); + }; + + /** + * Get the number of layer groups. + * + * @returns {number} The number of groups. + */ + this.getNumberOfLayerGroups = function () { + return stage.getNumberOfLayerGroups(); }; /** @@ -177,21 +217,27 @@ dwv.App = function () { /** * Initialise the application. * - * @param {object} opt The application options. + * @param {object} opt The application option with: + * - `dataViewConfigs`: data indexed object containing the data view + * configurations in the form of a list of objects containing: + * - divId: the HTML div id + * - orientation: optional 'axial', 'coronal' or 'sagittal' otientation + * string (default undefined keeps the original slice order) + * - `binders`: array of layerGroup binders + * - `tools`: tool name indexed object containing individual tool + * configurations + * - `viewOnFirstLoadItem`: boolean flag to trigger the first data render + * after the first loaded data or not + * - `defaultCharacterSet`: the default chraracter set string used for DICOM + * parsing */ this.init = function (opt) { // store options = opt; // defaults - if (typeof options.containerDivId === 'undefined') { - options.containerDivId = 'dwv'; - } if (typeof options.viewOnFirstLoadItem === 'undefined') { options.viewOnFirstLoadItem = true; } - if (typeof options.nSimultaneousData === 'undefined') { - options.nSimultaneousData = 1; - } // undo stack undoStack = new dwv.tool.UndoStack(); @@ -255,11 +301,11 @@ dwv.App = function () { } } // add tools to the controller - toolboxController = new dwv.ToolboxController(toolList); + toolboxController = new dwv.ctrl.ToolboxController(toolList); } // create load controller - loadController = new dwv.LoadController(options.defaultCharacterSet); + loadController = new dwv.ctrl.LoadController(options.defaultCharacterSet); loadController.onloadstart = onloadstart; loadController.onprogress = onprogress; loadController.onloaditem = onloaditem; @@ -269,27 +315,23 @@ dwv.App = function () { loadController.onabort = onabort; // create data controller - dataController = new dwv.DataController(); - }; - - /** - * Get the size available for the layer container div. - * - * @returns {object} The available width and height: {width:X; height:Y}. - */ - this.getLayerContainerSize = function () { - var size = layerController.getLayerContainerSize(); - return {width: size.x, height: size.y}; + dataController = new dwv.ctrl.DataController(); + // create stage + stage = new dwv.gui.Stage(); + if (typeof options.binders !== 'undefined') { + stage.setBinders(options.binders); + } }; /** * Get a HTML element associated to the application. * - * @param {string} name The name or id to find. + * @param {string} _name The name or id to find. * @returns {object} The found element or null. + * @deprecated */ - this.getElement = function (name) { - return dwv.gui.getElement(options.containerDivId, name); + this.getElement = function (_name) { + return null; }; /** @@ -298,7 +340,7 @@ dwv.App = function () { this.reset = function () { // clear objects dataController.reset(); - layerController.empty(); + stage.empty(); // reset undo/redo if (undoStack) { undoStack = new dwv.tool.UndoStack(); @@ -312,8 +354,8 @@ dwv.App = function () { * Reset the layout of the application. */ this.resetLayout = function () { - layerController.reset(); - layerController.draw(); + stage.reset(); + stage.draw(); }; /** @@ -344,6 +386,8 @@ dwv.App = function () { * Load a list of files. Can be image files or a state file. * * @param {Array} files The list of files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information * @fires dwv.App#loadstart * @fires dwv.App#loadprogress * @fires dwv.App#loaditem @@ -351,8 +395,12 @@ dwv.App = function () { * @fires dwv.App#error * @fires dwv.App#abort */ - this.loadFiles = function (files) { - loadController.loadFiles(files); + this.loadFiles = function (files, options) { + if (files.length === 0) { + dwv.logger.warn('Ignoring empty input file list.'); + return; + } + loadController.loadFiles(files, options); }; /** @@ -371,6 +419,10 @@ dwv.App = function () { * @fires dwv.App#abort */ this.loadURLs = function (urls, options) { + if (urls.length === 0) { + dwv.logger.warn('Ignoring empty input url list.'); + return; + } loadController.loadURLs(urls, options); }; @@ -403,26 +455,112 @@ dwv.App = function () { * Fit the display to the given size. To be called once the image is loaded. */ this.fitToContainer = function () { - layerController.fitToContainer(); - layerController.draw(); - // update style - style.setBaseScale(layerController.getBaseScale()); + var layerGroup = stage.getActiveLayerGroup(); + if (layerGroup) { + layerGroup.fitToContainer(self.getLastImage().getGeometry()); + layerGroup.draw(); + // update style + //style.setBaseScale(layerGroup.getBaseScale()); + } }; /** * Init the Window/Level display */ this.initWLDisplay = function () { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); var controller = viewLayer.getViewController(); controller.initialise(); }; + /** + * Get the layer group configuration from a data index. + * Defaults to div id 'layerGroup' if no association object has been set. + * + * @param {number} dataIndex The data index. + * @returns {Array} The list of associated configs. + */ + function getViewConfigs(dataIndex) { + // check options + if (options.dataViewConfigs === null || + typeof options.dataViewConfigs === 'undefined') { + throw new Error('No available data iew configuration'); + } + var configs = null; + if (typeof options.dataViewConfigs['*'] !== 'undefined') { + configs = options.dataViewConfigs['*']; + } else { + configs = options.dataViewConfigs[dataIndex]; + } + return configs; + } + + /** + * Set the data view configuration (see the init options for details). + * + * @param {object} configs The configuration list. + */ + this.setDataViewConfig = function (configs) { + // clean up + stage.empty(); + // set new + options.dataViewConfigs = configs; + // re-bind layers + stage.bindLayerGroups(); + }; + + /** + * Set the layer groups binders. + * + * @param {Array} list The binders list. + */ + this.setLayerGroupsBinders = function (list) { + stage.setBinders(list); + }; + /** * Render the current data. + * + * @param {number} dataIndex The data index to render. */ - this.render = function () { - layerController.draw(); + this.render = function (dataIndex) { + if (typeof dataIndex === 'undefined' || dataIndex === null) { + throw new Error('Cannot render without data index'); + } + // loop on all configs + var viewConfigs = getViewConfigs(dataIndex); + if (!viewConfigs) { + throw new Error('No view config for data: ' + dataIndex); + } + for (var i = 0; i < viewConfigs.length; ++i) { + var config = viewConfigs[i]; + // create layer group if not done yet + // warn: needs a loaded DOM + var layerGroup = + stage.getLayerGroupWithElementId(config.divId); + if (!layerGroup) { + // create new layer group + var element = document.getElementById(config.divId); + layerGroup = stage.addLayerGroup(element); + // bind events + bindLayerGroup(layerGroup); + // optional orientation + if (typeof config.orientation !== 'undefined') { + layerGroup.setTargetOrientation( + dwv.math.getMatrixFromName(config.orientation)); + } + } + // initialise or add view + if (layerGroup.getViewLayersByDataIndex(dataIndex).length === 0) { + if (layerGroup.getNumberOfLayers() === 0) { + initialiseBaseLayers(dataIndex, config.divId); + } else { + addViewLayer(dataIndex, config.divId); + } + } + // draw + layerGroup.draw(); + } }; /** @@ -433,8 +571,11 @@ dwv.App = function () { * @param {number} cy The zoom center Y coordinate. */ this.zoom = function (step, cx, cy) { - layerController.addScale(step, {x: cx, y: cy}); - layerController.draw(); + var layerGroup = stage.getActiveLayerGroup(); + var viewController = layerGroup.getActiveViewLayer().getViewController(); + var k = viewController.getCurrentScrollPosition(); + layerGroup.addScale(step, {x: cx, y: cy, z: k}); + layerGroup.draw(); }; /** @@ -444,8 +585,9 @@ dwv.App = function () { * @param {number} ty The translation along Y. */ this.translate = function (tx, ty) { - layerController.addTranslation({x: tx, y: ty}); - layerController.draw(); + var layerGroup = stage.getActiveLayerGroup(); + layerGroup.addTranslation({x: tx, y: ty}); + layerGroup.draw(); }; /** @@ -454,7 +596,7 @@ dwv.App = function () { * @param {number} alpha The opacity ([0:1] range). */ this.setOpacity = function (alpha) { - var viewLayer = layerController.getActiveViewLayer(); + var viewLayer = stage.getActiveLayerGroup().getActiveViewLayer(); viewLayer.setOpacity(alpha); viewLayer.draw(); }; @@ -462,11 +604,11 @@ dwv.App = function () { /** * Get the list of drawing display details. * - * @returns {object} The list of draw details including id, slice, frame... + * @returns {object} The list of draw details including id, position... */ this.getDrawDisplayDetails = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.getDrawDisplayDetails(); }; @@ -477,7 +619,7 @@ dwv.App = function () { */ this.getDrawStoreDetails = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.getDrawStoreDetails(); }; /** @@ -487,17 +629,17 @@ dwv.App = function () { * @param {Array} drawingsDetails An array of drawings details. */ this.setDrawings = function (drawings, drawingsDetails) { + var layerGroup = stage.getActiveLayerGroup(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); drawController.setDrawings( drawings, drawingsDetails, fireEvent, this.addToUndoStack); drawController.activateDrawLayer( - viewController.getCurrentPosition(), - viewController.getCurrentFrame()); + viewController.getCurrentOrientedPosition()); }; /** * Update a drawing from its details. @@ -506,7 +648,7 @@ dwv.App = function () { */ this.updateDraw = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.updateDraw(drawDetails); }; /** @@ -514,7 +656,7 @@ dwv.App = function () { */ this.deleteDraws = function () { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.deleteDraws(fireEvent, this.addToUndoStack); }; /** @@ -525,7 +667,7 @@ dwv.App = function () { */ this.isGroupVisible = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); return drawController.isGroupVisible(drawDetails); }; /** @@ -535,7 +677,7 @@ dwv.App = function () { */ this.toogleGroupVisibility = function (drawDetails) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + stage.getActiveLayerGroup().getActiveDrawLayer().getDrawController(); drawController.toogleGroupVisibility(drawDetails); }; @@ -545,7 +687,7 @@ dwv.App = function () { * @returns {object} The state of the app as a JSON object. */ this.getState = function () { - var state = new dwv.State(); + var state = new dwv.io.State(); return state.toJSON(self); }; @@ -585,10 +727,10 @@ dwv.App = function () { * Key down event handler example. * - CRTL-Z: undo * - CRTL-Y: redo - * - CRTL-ARROW_LEFT: next frame - * - CRTL-ARROW_UP: next slice - * - CRTL-ARROW_RIGHT: previous frame - * - CRTL-ARROW_DOWN: previous slice + * - CRTL-ARROW_LEFT: next element on fourth dim + * - CRTL-ARROW_UP: next element on third dim + * - CRTL-ARROW_RIGHT: previous element on fourth dim + * - CRTL-ARROW_DOWN: previous element on third dim * * @param {object} event The key down event. * @fires dwv.tool.UndoStack#undo @@ -596,20 +738,29 @@ dwv.App = function () { */ this.defaultOnKeydown = function (event) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); + var size = viewController.getImageSize(); if (event.ctrlKey) { if (event.keyCode === 37) { // crtl-arrow-left event.preventDefault(); - viewController.decrementFrameNb(); + if (size.moreThanOne(3)) { + viewController.decrementIndex(3); + } } else if (event.keyCode === 38) { // crtl-arrow-up event.preventDefault(); - viewController.incrementSliceNb(); + if (viewController.canScroll()) { + viewController.incrementScrollIndex(); + } } else if (event.keyCode === 39) { // crtl-arrow-right event.preventDefault(); - viewController.incrementFrameNb(); + if (size.moreThanOne(3)) { + viewController.incrementIndex(3); + } } else if (event.keyCode === 40) { // crtl-arrow-down event.preventDefault(); - viewController.decrementSliceNb(); + if (viewController.canScroll()) { + viewController.decrementScrollIndex(); + } } else if (event.keyCode === 89) { // crtl-y undoStack.redo(); } else if (event.keyCode === 90) { // crtl-z @@ -642,7 +793,7 @@ dwv.App = function () { */ this.setColourMap = function (colourMap) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); viewController.setColourMapFromName(colourMap); }; @@ -653,7 +804,7 @@ dwv.App = function () { */ this.setWindowLevelPreset = function (preset) { var viewController = - layerController.getActiveViewLayer().getViewController(); + stage.getActiveLayerGroup().getActiveViewLayer().getViewController(); viewController.setWindowLevelPreset(preset); }; @@ -663,24 +814,33 @@ dwv.App = function () { * @param {string} tool The tool. */ this.setTool = function (tool) { - var layer = null; - var previousLayer = null; - if (tool === 'Draw' || - tool === 'Livewire' || - tool === 'Floodfill') { - layer = layerController.getActiveDrawLayer(); - previousLayer = layerController.getActiveViewLayer(); - } else { - layer = layerController.getActiveViewLayer(); - previousLayer = layerController.getActiveDrawLayer(); - } - if (previousLayer) { - toolboxController.detachLayer(previousLayer); + // bind tool to layer: not really important which layer since + // tools are responsible for finding the event source layer + // but there needs to be at least one binding... + for (var i = 0; i < stage.getNumberOfLayerGroups(); ++i) { + var layerGroup = stage.getLayerGroup(i); + // unbind previous layer + var vl = layerGroup.getActiveViewLayer(); + if (vl) { + toolboxController.unbindLayer(vl); + } + var dl = layerGroup.getActiveDrawLayer(); + if (dl) { + toolboxController.unbindLayer(dl); + } + // bind new layer + var layer = null; + if (tool === 'Draw' || + tool === 'Livewire' || + tool === 'Floodfill') { + layer = layerGroup.getActiveDrawLayer(); + } else { + layer = layerGroup.getActiveViewLayer(); + } + toolboxController.bindLayer(layer); } - // detach to avoid possible double attach - toolboxController.detachLayer(layer); - toolboxController.attachLayer(layer); + // set toolbox tool toolboxController.setSelectedTool(tool); }; @@ -765,13 +925,6 @@ dwv.App = function () { * @private */ function onloadstart(event) { - isFirstLoadItem = true; - - if (event.loadtype === 'image' && - dataController.length() === options.nSimultaneousData) { - self.reset(); - } - /** * Load start event. * @@ -818,27 +971,31 @@ dwv.App = function () { function onloaditem(event) { // check event if (typeof event.data === 'undefined') { - dwv.logger.error('Missing loaditem event data ' + event); + dwv.logger.error('Missing loaditem event data.'); } if (typeof event.loadtype === 'undefined') { - dwv.logger.error('Missing loaditem event load type ' + event); + dwv.logger.error('Missing loaditem event load type.'); } - // number returned by image.appendSlice - var sliceNb = null; + var isFirstLoadItem = event.isfirstitem; + var isTimepoint = typeof event.timepoint !== 'undefined'; + var timeId = 0; + if (isTimepoint) { + timeId = event.timepoint.id; + } var eventMetaData = null; if (event.loadtype === 'image') { - if (isFirstLoadItem) { + if (isFirstLoadItem && timeId === 0) { dataController.addNew(event.data.image, event.data.info); } else { - sliceNb = dataController.updateCurrent( - event.data.image, event.data.info); + dataController.update( + event.loadid, event.data.image, event.data.info, + timeId); } - eventMetaData = event.data.info; } else if (event.loadtype === 'state') { - var state = new dwv.State(); + var state = new dwv.io.State(); state.apply(self, state.fromJSON(event.data)); eventMetaData = 'state'; } @@ -861,45 +1018,11 @@ dwv.App = function () { loadtype: event.loadtype }); - // adapt context - if (event.loadtype === 'image') { - if (isFirstLoadItem) { - // create layer controller if not done yet - // warn: needs a loaded DOM - if (!layerController) { - layerController = - new dwv.LayerController(self.getElement('layerContainer')); - } - // initialise or add view - var dataIndex = dataController.getCurrentIndex(); - var data = dataController.get(dataIndex); - if (layerController.getNumberOfLayers() === 0) { - initialiseBaseLayers(data.image, data.meta, dataIndex); - } else { - addViewLayer(data.image, data.meta, dataIndex); - } - } else { - // update slice number if new slice was inserted before - var controller = - layerController.getActiveViewLayer().getViewController(); - var currentPosition = controller.getCurrentPosition(); - if (sliceNb <= currentPosition.k) { - controller.setCurrentPosition({ - i: currentPosition.i, - j: currentPosition.j, - k: currentPosition.k + 1 - }, true); - } - } - - // render if flag allows - if (isFirstLoadItem && options.viewOnFirstLoadItem) { - self.render(); - } + // render if first and flag allows + if (event.loadtype === 'image' && + isFirstLoadItem && options.viewOnFirstLoadItem) { + self.render(event.loadid); } - - // reset flag - isFirstLoadItem = false; } /** @@ -928,7 +1051,6 @@ dwv.App = function () { * @private */ function onloadend(event) { - isFirstLoadItem = null; /** * Main load end event: fired when the load finishes, * successfully or not. @@ -989,97 +1111,139 @@ dwv.App = function () { } /** - * Bind view layer events to app. + * Bind layer group events to app. * - * @param {object} viewLayer The view layer. + * @param {object} group The layer group. * @private */ - function bindViewLayer(viewLayer) { - // propagate view events - viewLayer.propagateViewEvents(true); - for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - viewLayer.addEventListener(dwv.image.viewEventNames[j], fireEvent); - } + function bindLayerGroup(group) { + // propagate layer group events + group.addEventListener('zoomchange', fireEvent); + group.addEventListener('offsetchange', fireEvent); // propagate viewLayer events - viewLayer.addEventListener('renderstart', fireEvent); - viewLayer.addEventListener('renderend', fireEvent); - } - - /** - * Un-Bind view layer events from app. - * - * @param {object} viewLayer The view layer. - * @private - */ - function unbindViewLayer(viewLayer) { - // stop propagating view events - viewLayer.propagateViewEvents(false); + group.addEventListener('renderstart', fireEvent); + group.addEventListener('renderend', fireEvent); + // propagate view events for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - viewLayer.removeEventListener(dwv.image.viewEventNames[j], fireEvent); + group.addEventListener(dwv.image.viewEventNames[j], fireEvent); } - // stop propagating viewLayer events - viewLayer.removeEventListener('renderstart', fireEvent); - viewLayer.removeEventListener('renderend', fireEvent); } /** * Initialise the layers. * To be called once the DICOM data has been loaded. * - * @param {object} image The image to view. - * @param {object} meta The image meta data. * @param {number} dataIndex The data index. + * @param {string} layerGroupElementId The layer group element id. * @private */ - function initialiseBaseLayers(image, meta, dataIndex) { - // view layer - var viewLayer = layerController.addViewLayer(); - // optional draw layer - if (toolboxController && toolboxController.hasTool('Draw')) { - layerController.addDrawLayer(); + function initialiseBaseLayers(dataIndex, layerGroupElementId) { + var data = dataController.get(dataIndex); + if (!data) { + throw new Error('Cannot initialise layers with data id: ' + dataIndex); + } + var layerGroup = stage.getLayerGroupWithElementId(layerGroupElementId); + if (!layerGroup) { + throw new Error('Cannot initialise layers with group id: ' + + layerGroupElementId); } - // initialise layers - layerController.initialise(image, meta, dataIndex); - - // update style - style.setBaseScale(layerController.getBaseScale()); - // bind view to app - bindViewLayer(viewLayer); - // propagate layer events - layerController.addEventListener('zoomchange', fireEvent); - layerController.addEventListener('offsetchange', fireEvent); + // add layers + addViewLayer(dataIndex, layerGroupElementId); - // listen to image changes - dataController.addEventListener('imagechange', viewLayer.onimagechange); + // update style + //style.setBaseScale(layerGroup.getBaseScale()); // initialise the toolbox if (toolboxController) { - toolboxController.init(layerController.displayToIndex); + toolboxController.init(); } } /** * Add a view layer. * - * @param {object} image The image to view. - * @param {object} meta The image meta data. * @param {number} dataIndex The data index. + * @param {string} layerGroupElementId The layer group element id. */ - function addViewLayer(image, meta, dataIndex) { - // un-bind previous - unbindViewLayer(layerController.getActiveViewLayer()); + function addViewLayer(dataIndex, layerGroupElementId) { + var data = dataController.get(dataIndex); + if (!data) { + throw new Error('Cannot initialise layers with data id: ' + dataIndex); + } + var layerGroup = stage.getLayerGroupWithElementId(layerGroupElementId); + if (!layerGroup) { + throw new Error('Cannot initialise layers with group id: ' + + layerGroupElementId); + } + var imageGeometry = data.image.getGeometry(); + + // un-bind + stage.unbindLayerGroups(); + + // create and setup view + var viewFactory = new dwv.ViewFactory(); + var view = viewFactory.create( + new dwv.dicom.DicomElementsWrapper(data.meta), + data.image); + var viewOrientation = dwv.gui.getViewOrientation( + imageGeometry, + layerGroup.getTargetOrientation() + ); + view.setOrientation(viewOrientation); + + // TODO: find another way for a default colour map + var opacity = 1; + if (dataIndex !== 0) { + view.setColourMap(dwv.image.lut.rainbow); + opacity = 0.5; + } + + // view layer + var viewLayer = layerGroup.addViewLayer(); + viewLayer.setView(view); + var size2D = imageGeometry.getSize(viewOrientation).get2D(); + var spacing2D = imageGeometry.getSpacing(viewOrientation).get2D(); + viewLayer.initialise(size2D, spacing2D, dataIndex); + viewLayer.setOpacity(opacity); + + // compensate origin difference + var diff = null; + if (dataIndex !== 0) { + var data0 = dataController.get(0); + var origin0 = data0.image.getGeometry().getOrigin(); + var origin1 = imageGeometry.getOrigin(); + diff = origin0.minus(origin1); + viewLayer.setBaseOffset(diff); + } - var viewLayer = layerController.addViewLayer(); - // initialise - viewLayer.initialise(image, meta, dataIndex); - // apply layer scale - viewLayer.resize(layerController.getScale()); // listen to image changes dataController.addEventListener('imagechange', viewLayer.onimagechange); - // bind new - bindViewLayer(viewLayer); + // bind + stage.bindLayerGroups(); + + // optional draw layer + if (toolboxController && toolboxController.hasTool('Draw')) { + var dl = layerGroup.addDrawLayer(); + dl.initialise(size2D, spacing2D, dataIndex); + dl.setPlaneHelper(viewLayer.getViewController().getPlaneHelper()); + + var vc = viewLayer.getViewController(); + // positionchange event like data + var value = [ + vc.getCurrentIndex().getValues(), + vc.getCurrentPosition().getValues() + ]; + layerGroup.updateLayersToPositionChange({value: value}); + + // compensate origin difference + if (dataIndex !== 0) { + dl.setBaseOffset(diff); + } + } + + layerGroup.fitToContainer(); } }; diff --git a/src/app/dataController.js b/src/app/dataController.js index 9ad77aad5e..d3058c9ad8 100644 --- a/src/app/dataController.js +++ b/src/app/dataController.js @@ -1,12 +1,14 @@ // namespaces var dwv = dwv || {}; +/** @namespace */ +dwv.ctrl = dwv.ctrl || {}; /* * Data (list of {image, meta}) controller. * * @class */ -dwv.DataController = function () { +dwv.ctrl.DataController = function () { /** * List of {image, meta}. @@ -16,18 +18,10 @@ dwv.DataController = function () { */ var data = []; - /** - * Current data index. - * - * @private - * @type {number} - */ - var currentIndex = null; - /** * Listener handler. * - * @type {object} + * @type {dwv.utils.ListenerHandler} * @private */ var listenerHandler = new dwv.utils.ListenerHandler(); @@ -45,7 +39,6 @@ dwv.DataController = function () { * Reset the class: empty the data storage. */ this.reset = function () { - currentIndex = null; data = []; }; @@ -59,22 +52,13 @@ dwv.DataController = function () { return data[index]; }; - /** - * Get the current data index. - * - * @returns {number} The index. - */ - this.getCurrentIndex = function () { - return currentIndex; - }; - /** * Set the image at a given index. * - * @param {object} image The image to set. * @param {number} index The index of the data. + * @param {dwv.image.Image} image The image to set. */ - this.setImage = function (image, index) { + this.setImage = function (index, image) { data[index].image = image; fireEvent({ type: 'imagechange', @@ -85,11 +69,10 @@ dwv.DataController = function () { /** * Add a new data. * - * @param {object} image The image. + * @param {dwv.image.Image} image The image. * @param {object} meta The image meta. */ this.addNew = function (image, meta) { - currentIndex = data.length; // store the new image data.push({ image: image, @@ -100,29 +83,43 @@ dwv.DataController = function () { /** * Update the current data. * - * @param {object} image The image. + * @param {number} index The index of the data. + * @param {dwv.image.Image} image The image. * @param {object} meta The image meta. - * @returns {number} The slice number at which the image was added. + * @param {number} timeId The time ID. */ - this.updateCurrent = function (image, meta) { - var currentData = data[currentIndex]; + this.update = function (index, image, meta, timeId) { + var dataToUpdate = data[index]; + + // handle possible timepoint + if (typeof timeId !== 'undefined') { + var size = dataToUpdate.image.getGeometry().getSize(); + // append frame for first frame (still 3D) or other frames + if ((size.length() === 3 && timeId !== 0) || + (size.length() > 3 && timeId >= size.get(3))) { + dataToUpdate.image.appendFrame(); + } + } + // add slice to current image - var sliceNb = currentData.image.appendSlice(image); + dataToUpdate.image.appendSlice(image, timeId); + // update meta data - var idKey = ''; - if (typeof meta.x00020010 !== 'undefined') { - // dicom case - idKey = 'InstanceNumber'; - } else { - idKey = 'imageUid'; + // TODO add time support + if (timeId === 0) { + var idKey = ''; + if (typeof meta.x00020010 !== 'undefined') { + // dicom case + idKey = 'InstanceNumber'; + } else { + idKey = 'imageUid'; + } + dataToUpdate.meta = dwv.utils.mergeObjects( + dataToUpdate.meta, + getMetaObject(meta), + idKey, + 'value'); } - currentData.meta = dwv.utils.mergeObjects( - currentData.meta, - getMetaObject(meta), - idKey, - 'value'); - - return sliceNb; }; /** diff --git a/src/app/drawController.js b/src/app/drawController.js index abff91f066..cf5c8f9331 100644 --- a/src/app/drawController.js +++ b/src/app/drawController.js @@ -1,6 +1,8 @@ // namespaces var dwv = dwv || {}; dwv.draw = dwv.draw || {}; +dwv.ctrl = dwv.ctrl || {}; + /** * The Konva namespace. * @@ -12,11 +14,14 @@ var Konva = Konva || {}; /** * Get the draw group id for a given position. * - * @param {number} sliceNumber The slice number. - * @param {number} frameNumber The frame number. - * @returns {number} The group id. + * @param {dwv.math.Point} currentPosition The current position. + * @returns {string} The group id. + * @deprecated Use the index.toStringId instead. */ -dwv.draw.getDrawPositionGroupId = function (sliceNumber, frameNumber) { +dwv.draw.getDrawPositionGroupId = function (currentPosition) { + var sliceNumber = currentPosition.get(2); + var frameNumber = currentPosition.length() === 4 + ? currentPosition.get(3) : 0; return 'slice-' + sliceNumber + '_frame-' + frameNumber; }; @@ -25,6 +30,7 @@ dwv.draw.getDrawPositionGroupId = function (sliceNumber, frameNumber) { * * @param {string} groupId The group id. * @returns {object} The slice and frame number. + * @deprecated Use the dwv.math.getVectorFromStringId instead. */ dwv.draw.getPositionFromGroupId = function (groupId) { var sepIndex = groupId.indexOf('_'); @@ -124,7 +130,7 @@ dwv.draw.getHierarchyLog = function (layer, prefix) { * @class * @param {object} konvaLayer The draw layer. */ -dwv.DrawController = function (konvaLayer) { +dwv.ctrl.DrawController = function (konvaLayer) { // current position group id var currentPosGroupId = null; @@ -167,16 +173,17 @@ dwv.DrawController = function (konvaLayer) { /** * Activate the current draw layer. * - * @param {object} currentPosition The current {i,j,k} position. - * @param {number} currentFrame The current frame number. + * @param {dwv.math.Index} index The current position. + * @param {number} scrollIndex The scroll index. */ - this.activateDrawLayer = function (currentPosition, currentFrame) { - // set current position - var currentSlice = currentPosition.k; - // var currentFrame = viewController.getCurrentFrame(); + this.activateDrawLayer = function (index, scrollIndex) { + // TODO: add layer info // get and store the position group id - currentPosGroupId = dwv.draw.getDrawPositionGroupId( - currentSlice, currentFrame); + var dims = [scrollIndex]; + for (var j = 3; j < index.length(); ++j) { + dims.push(j); + } + currentPosGroupId = index.toStringId(dims); // get all position groups var posGroups = konvaLayer.getChildren(dwv.draw.isPositionNode); @@ -198,13 +205,14 @@ dwv.DrawController = function (konvaLayer) { /** * Get a list of drawing display details. * - * @returns {object} A list of draw details including id, slice, frame... + * @returns {Array} A list of draw details as + * {id, position, type, color, meta} */ this.getDrawDisplayDetails = function () { var list = []; var groups = konvaLayer.getChildren(); for (var j = 0, lenj = groups.length; j < lenj; ++j) { - var position = dwv.draw.getPositionFromGroupId(groups[j].id()); + var position = dwv.math.getIndexFromStringId(groups[j].id()); var collec = groups[j].getChildren(); for (var i = 0, leni = collec.length; i < leni; ++i) { var shape = collec[i].getChildren(dwv.draw.isNodeNameShape)[0]; @@ -229,8 +237,7 @@ dwv.DrawController = function (konvaLayer) { } list.push({ id: collec[i].id(), - slice: position.sliceNumber, - frame: position.frameNumber, + position: position.toString(), type: type, color: shape.stroke(), meta: text.meta @@ -495,4 +502,4 @@ dwv.DrawController = function (konvaLayer) { } }; -}; // class dwv.DrawController +}; // class DrawController diff --git a/src/app/layerController.js b/src/app/layerController.js deleted file mode 100644 index 9acf401ff2..0000000000 --- a/src/app/layerController.js +++ /dev/null @@ -1,513 +0,0 @@ -// namespaces -var dwv = dwv || {}; -dwv.gui = dwv.gui || {}; - -/** - * Layer controller. - * - * @param {object} containerDiv The layer div. - * @class - */ -dwv.LayerController = function (containerDiv) { - - var layers = []; - - /** - * The layer scale as {x,y}. - * - * @private - * @type {object} - */ - var scale = {x: 1, y: 1}; - - /** - * The base scale as {x,y}: all posterior scale will be on top of this one. - * - * @private - * @type {object} - */ - var baseScale = {x: 1, y: 1}; - - /** - * The layer offset as {x,y}. - * - * @private - * @type {object} - */ - var offset = {x: 0, y: 0}; - - /** - * The layer size as {x,y}. - * - * @private - * @type {object} - */ - var layerSize = dwv.gui.getDivSize(containerDiv); - - /** - * Active view layer index. - * - * @private - * @type {number} - */ - var activeViewLayerIndex = null; - - /** - * Active draw layer index. - * - * @private - * @type {number} - */ - var activeDrawLayerIndex = null; - - /** - * Listener handler. - * - * @type {object} - * @private - */ - var listenerHandler = new dwv.utils.ListenerHandler(); - - /** - * Get the layer scale. - * - * @returns {object} The scale as {x,y}. - */ - this.getScale = function () { - return scale; - }; - - /** - * Get the base scale. - * - * @returns {object} The scale as {x,y}. - */ - this.getBaseScale = function () { - return baseScale; - }; - - /** - * Get the added scale: the scale added to the base scale - * - * @returns {object} The scale as {x,y}. - */ - this.getAddedScale = function () { - return { - x: scale.x / baseScale.x, - y: scale.y / baseScale.y - }; - }; - - /** - * Get the layer offset. - * - * @returns {object} The offset as {x,y}. - */ - this.getOffset = function () { - return offset; - }; - - /** - * Transform a display position to an index. - * - * @param {dwv.Math.Point2D} point2D The point to convert. - * @returns {object} The equivalent index. - */ - this.displayToIndex = function (point2D) { - return { - x: point2D.x / scale.x + offset.x, - y: point2D.y / scale.y + offset.y - }; - }; - - /** - * Get the number of layers handled by this class. - * - * @returns {number} The number of layers. - */ - this.getNumberOfLayers = function () { - return layers.length; - }; - - /** - * Get the active image layer. - * - * @returns {object} The layer. - */ - this.getActiveViewLayer = function () { - return layers[activeViewLayerIndex]; - }; - - /** - * Get the active draw layer. - * - * @returns {object} The layer. - */ - this.getActiveDrawLayer = function () { - return layers[activeDrawLayerIndex]; - }; - - /** - * Set the active view layer. - * - * @param {number} index The index of the layer to set as active. - */ - this.setActiveViewLayer = function (index) { - // un-bind previous layer - var viewLayer0 = this.getActiveViewLayer(); - if (viewLayer0) { - viewLayer0.removeEventListener( - 'slicechange', this.updatePosition); - viewLayer0.removeEventListener( - 'framechange', this.updatePosition); - } - - // set index - activeViewLayerIndex = index; - - // bind new layer - var viewLayer = this.getActiveViewLayer(); - viewLayer.addEventListener( - 'slicechange', this.updatePosition); - viewLayer.addEventListener( - 'framechange', this.updatePosition); - }; - - /** - * Set the active draw layer. - * - * @param {number} index The index of the layer to set as active. - */ - this.setActiveDrawLayer = function (index) { - activeDrawLayerIndex = index; - }; - - /** - * Add a view layer. - * - * @returns {object} The created layer. - */ - this.addViewLayer = function () { - // layer index - var viewLayerIndex = layers.length; - // create div - var div = getNextLayerDiv(); - // prepend to container - containerDiv.append(div); - // view layer - var layer = new dwv.gui.ViewLayer(div); - // set z-index: last on top - layer.setZIndex(viewLayerIndex); - // add layer - layers.push(layer); - // mark it as active - this.setActiveViewLayer(viewLayerIndex); - // return - return layer; - }; - - /** - * Add a draw layer. - * - * @returns {object} The created layer. - */ - this.addDrawLayer = function () { - // store active index - activeDrawLayerIndex = layers.length; - // create div - var div = getNextLayerDiv(); - // prepend to container - containerDiv.append(div); - // draw layer - var layer = new dwv.gui.DrawLayer(div); - // set z-index: above view + last on top - layer.setZIndex(10 + activeDrawLayerIndex); - // add layer - layers.push(layer); - // return - return layer; - }; - - /** - * Get the next layer DOM div. - * - * @returns {object} A DOM div. - */ - function getNextLayerDiv() { - var div = document.createElement('div'); - div.id = 'layer' + layers.length; - div.className = 'layer'; - div.style.pointerEvents = 'none'; - return div; - } - - /** - * Empty the layer list. - */ - this.empty = function () { - layers = []; - // reset active indices - activeViewLayerIndex = null; - activeDrawLayerIndex = null; - // clean container div - var previous = containerDiv.getElementsByClassName('layer'); - if (previous) { - while (previous.length > 0) { - previous[0].remove(); - } - } - }; - - /** - * Update layers to the active view position. - */ - this.updatePosition = function () { - var viewController = - layers[activeViewLayerIndex].getViewController(); - var pos = [ - viewController.getCurrentPosition(), - viewController.getCurrentFrame() - ]; - for (var i = 0; i < layers.length; ++i) { - if (i !== activeViewLayerIndex) { - layers[i].updatePosition(pos); - } - } - }; - - /** - * Get the fit to container scale. - * To be called once the image is loaded. - * - * @returns {number} The scale. - */ - this.getFitToContainerScale = function () { - // get container size - var size = this.getLayerContainerSize(); - // best fit - return Math.min( - (size.x / layerSize.x), - (size.y / layerSize.y) - ); - }; - - /** - * Fit the display to the size of the container. - * To be called once the image is loaded. - */ - this.fitToContainer = function () { - var fitScale = this.getFitToContainerScale(); - this.resize({x: fitScale, y: fitScale}); - }; - - /** - * Get the size available for the layer container div. - * - * @returns {object} The available width and height as {width,height}. - */ - this.getLayerContainerSize = function () { - return dwv.gui.getDivSize(containerDiv); - }; - - /** - * Add scale to the layers. Scale cannot go lower than 0.1. - * - * @param {object} scaleStep The scale to add. - * @param {object} center The scale center point as {x,y}. - */ - this.addScale = function (scaleStep, center) { - var newScale = { - x: Math.max(scale.x + scaleStep, 0.1), - y: Math.max(scale.y + scaleStep, 0.1) - }; - // center should stay the same: - // newOffset + center / newScale = oldOffset + center / oldScale - this.setOffset({ - x: (center.x / scale.x) + offset.x - (center.x / newScale.x), - y: (center.y / scale.y) + offset.y - (center.y / newScale.y) - }); - this.setScale(newScale); - }; - - /** - * Set the layers' scale. - * - * @param {object} newScale The scale to apply as {x,y}. - * @fires dwv.LayerController#zoomchange - */ - this.setScale = function (newScale) { - scale = newScale; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].setScale(scale); - } - - /** - * Zoom change event. - * - * @event dwv.LayerController#zoomchange - * @type {object} - * @property {Array} value The changed value. - */ - fireEvent({ - type: 'zoomchange', - value: [scale.x, scale.y], - }); - }; - - /** - * Add translation to the layers. - * - * @param {object} translation The translation as {x,y}. - */ - this.addTranslation = function (translation) { - this.setOffset({ - x: offset.x - translation.x / scale.x, - y: offset.y - translation.y / scale.y - }); - }; - - /** - * Set the layers' offset. - * - * @param {object} newOffset The offset as {x,y}. - * @fires dwv.LayerController#offsetchange - */ - this.setOffset = function (newOffset) { - // store - offset = newOffset; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].setOffset(offset); - } - - /** - * Offset change event. - * - * @event dwv.LayerController#offsetchange - * @type {object} - * @property {Array} value The changed value. - */ - fireEvent({ - type: 'offsetchange', - value: [offset.x, offset.y], - }); - }; - - /** - * Initialise the layer: set the canvas and context - * - * @param {object} image The image. - * @param {object} metaData The image meta data. - * @param {number} dataIndex The data index. - */ - this.initialise = function (image, metaData, dataIndex) { - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; - // apply to layers - for (var i = 0; i < layers.length; ++i) { - layers[i].initialise(image, metaData, dataIndex); - } - // first position update - this.updatePosition(); - // fit data - this.fitToContainer(); - }; - - /** - * Reset the stage to its initial scale and no offset. - */ - this.reset = function () { - this.setScale(baseScale); - this.setOffset({x: 0, y: 0}); - }; - - /** - * Resize the layer: update the base scale and layer sizes. - * - * @param {number} newScale The scale as {x,y}. - */ - this.resize = function (newScale) { - // store - scale = { - x: scale.x * newScale.x / baseScale.x, - y: scale.y * newScale.y / baseScale.y - }; - baseScale = newScale; - - // resize container - var width = parseInt(layerSize.x * baseScale.x, 10); - var height = parseInt(layerSize.y * baseScale.y, 10); - containerDiv.style.width = width + 'px'; - containerDiv.style.height = height + 'px'; - - // resize if test passes - if (dwv.gui.canCreateCanvas(width, height)) { - // call resize and scale on layers - for (var i = 0; i < layers.length; ++i) { - layers[i].resize(baseScale); - layers[i].setScale(scale); - } - } else { - dwv.logger.warn('Cannot create a ' + width + ' * ' + height + - ' canvas, trying half the size...'); - this.resize({x: newScale.x * 0.5, y: newScale.y * 0.5}); - } - }; - - /** - * Draw the layer. - */ - this.draw = function () { - for (var i = 0; i < layers.length; ++i) { - layers[i].draw(); - } - }; - - /** - * Display the layer. - * - * @param {boolean} flag Whether to display the layer or not. - */ - this.display = function (flag) { - for (var i = 0; i < layers.length; ++i) { - layers[i].display(flag); - } - }; - - /** - * Add an event listener to this class. - * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type, will be called with the fired event. - */ - this.addEventListener = function (type, callback) { - listenerHandler.add(type, callback); - }; - - /** - * Remove an event listener from this class. - * - * @param {string} type The event type. - * @param {object} callback The method associated with the provided - * event type. - */ - this.removeEventListener = function (type, callback) { - listenerHandler.remove(type, callback); - }; - - /** - * Fire an event: call all associated listeners with the input event object. - * - * @param {object} event The event to fire. - * @private - */ - function fireEvent(event) { - listenerHandler.fireEvent(event); - } - -}; // LayerController class diff --git a/src/app/loadController.js b/src/app/loadController.js index 1b08060d22..8d46401ef9 100644 --- a/src/app/loadController.js +++ b/src/app/loadController.js @@ -1,5 +1,6 @@ // namespaces var dwv = dwv || {}; +dwv.ctrl = dwv.ctrl || {}; /** * Load controller. @@ -7,26 +8,39 @@ var dwv = dwv || {}; * @param {string} defaultCharacterSet The default character set. * @class */ -dwv.LoadController = function (defaultCharacterSet) { +dwv.ctrl.LoadController = function (defaultCharacterSet) { // closure to self var self = this; - // current loader - var currentLoader = null; - // Is the data mono-slice? - var isMonoSliceData = null; + // current loaders + var currentLoaders = {}; + + // load counter + var counter = -1; + + /** + * Get the next load id. + * + * @returns {number} The next id. + */ + function getNextLoadId() { + ++counter; + return counter; + } /** * Load a list of files. Can be image files or a state file. * * @param {Array} files The list of files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information */ - this.loadFiles = function (files) { + this.loadFiles = function (files, options) { // has been checked for emptiness. var ext = files[0].name.split('.').pop().toLowerCase(); if (ext === 'json') { - loadStateFile(files[0]); + loadStateFile(files[0], options); } else { - loadImageFiles(files); + loadImageFiles(files, options); } }; @@ -57,44 +71,37 @@ dwv.LoadController = function (defaultCharacterSet) { this.loadImageObject = function (data) { // create IO var memoryIO = new dwv.io.MemoryLoader(); - // create options - var options = {}; // load data - loadImageData(data, memoryIO, options); + loadData(data, memoryIO, 'image'); }; /** - * Abort the current load. + * Abort the current loaders. */ this.abort = function () { - if (currentLoader) { - currentLoader.abort(); - currentLoader = null; + var keys = Object.keys(currentLoaders); + for (var i = 0; i < keys.length; ++i) { + currentLoaders[i].loader.abort(); + delete currentLoaders[i]; } }; - /** - * Is the data mono-slice? - * - * @returns {boolean} True if the data only contains one slice. - */ - this.isMonoSliceData = function () { - return isMonoSliceData; - }; - // private ---------------------------------------------------------------- /** * Load a list of image files. * * @param {Array} files The list of image files to load. + * @param {object} options The options object, can contain: + * - timepoint: an object with time information * @private */ - function loadImageFiles(files) { + function loadImageFiles(files, options) { // create IO var fileIO = new dwv.io.FilesLoader(); + fileIO.setDefaultCharacterSet(defaultCharacterSet); // load data - loadImageData(files, fileIO); + loadData(files, fileIO, 'image', options); } /** @@ -109,21 +116,23 @@ dwv.LoadController = function (defaultCharacterSet) { function loadImageUrls(urls, options) { // create IO var urlIO = new dwv.io.UrlsLoader(); + urlIO.setDefaultCharacterSet(defaultCharacterSet); // load data - loadImageData(urls, urlIO, options); + loadData(urls, urlIO, 'image', options); } /** * Load a State file. * * @param {string} file The state file to load. + * @param {object} options The options object. * @private */ - function loadStateFile(file) { + function loadStateFile(file, options) { // create IO var fileIO = new dwv.io.FilesLoader(); // load data - loadStateData([file], fileIO); + loadData([file], fileIO, 'state', options); } /** @@ -139,77 +148,87 @@ dwv.LoadController = function (defaultCharacterSet) { // create IO var urlIO = new dwv.io.UrlsLoader(); // load data - loadStateData([url], urlIO, options); + loadData([url], urlIO, 'state', options); } /** - * Load a list of image data. + * Load a list of data. * * @param {Array} data Array of data to load. * @param {object} loader The data loader. + * @param {string} loadType The data load type: 'image' or 'state'. * @param {object} options Options passed to the final loader. * @private */ - function loadImageData(data, loader, options) { - // first data name - var firstName = ''; - if (typeof data[0].name !== 'undefined') { - firstName = data[0].name; - } else { - firstName = data[0]; + function loadData(data, loader, loadType, options) { + var eventInfo = { + loadtype: loadType, + }; + + // check if timepoint + var hasTimepoint = false; + if (typeof options !== 'undefined' && + typeof options.timepoint !== 'undefined') { + hasTimepoint = true; } - // flag used by scroll to decide wether to activate or not - // TODO: supposing multi-slice for zip files, could not be... - isMonoSliceData = (data.length === 1 && - firstName.split('.').pop().toLowerCase() !== 'zip' && - !dwv.utils.endsWith(firstName, 'DICOMDIR') && - !dwv.utils.endsWith(firstName, '.dcmdir')); + var loadId = null; + if (hasTimepoint) { + loadId = options.timepoint.dataId; + eventInfo.timepoint = options.timepoint; + } else { + loadId = getNextLoadId(); + } + eventInfo.loadid = loadId; - // set IO - var loadtype = 'image'; - loader.setDefaultCharacterSet(defaultCharacterSet); + // set callbacks loader.onloadstart = function (event) { // store loader to allow abort - currentLoader = loader; + currentLoaders[loadId] = { + loader: loader, + isFirstItem: true + }; // callback - augmentCallbackEvent(self.onloadstart, loadtype)(event); + augmentCallbackEvent(self.onloadstart, eventInfo)(event); }; - loader.onprogress = augmentCallbackEvent(self.onprogress, loadtype); - loader.onloaditem = augmentCallbackEvent(self.onloaditem, loadtype); - loader.onload = augmentCallbackEvent(self.onload, loadtype); + loader.onprogress = augmentCallbackEvent(self.onprogress, eventInfo); + loader.onloaditem = function (event) { + var isFirstItem = currentLoaders[loadId].isFirstItem; + var eventInfoItem = { + loadtype: loadType, + loadid: loadId, + isfirstitem: isFirstItem + }; + if (hasTimepoint) { + eventInfoItem.timepoint = options.timepoint; + } + augmentCallbackEvent(self.onloaditem, eventInfoItem)(event); + if (isFirstItem) { + currentLoaders[loadId].isFirstItem = false; + } + }; + loader.onload = augmentCallbackEvent(self.onload, eventInfo); loader.onloadend = function (event) { // reset current loader - currentLoader = null; + delete currentLoaders[loadId]; // callback - augmentCallbackEvent(self.onloadend, loadtype)(event); + augmentCallbackEvent(self.onloadend, eventInfo)(event); }; - loader.onerror = augmentCallbackEvent(self.onerror, loadtype); - loader.onabort = augmentCallbackEvent(self.onabort, loadtype); + loader.onerror = augmentCallbackEvent(self.onerror, eventInfo); + loader.onabort = augmentCallbackEvent(self.onabort, eventInfo); // launch load - loader.load(data, options); - } - - /** - * Load a State data. - * - * @param {Array} data Array of data to load. - * @param {object} loader The data loader. - * @param {object} options Options passed to the final loader. - * @private - */ - function loadStateData(data, loader, options) { - var loadtype = 'state'; - // set callbacks - loader.onloadstart = augmentCallbackEvent(self.onloadstart, loadtype); - loader.onprogress = augmentCallbackEvent(self.onprogress, loadtype); - loader.onloaditem = augmentCallbackEvent(self.onloaditem, loadtype); - loader.onload = augmentCallbackEvent(self.onload, loadtype); - loader.onloadend = augmentCallbackEvent(self.onloadend, loadtype); - loader.onerror = augmentCallbackEvent(self.onerror, loadtype); - loader.onabort = augmentCallbackEvent(self.onabort, loadtype); - // launch load - loader.load(data, options); + try { + loader.load(data, options); + } catch (error) { + self.onerror({ + error: error, + loadId: loadId + }); + self.onloadend({ + loadId: loadId + }); + return; + } } /** @@ -217,12 +236,16 @@ dwv.LoadController = function (defaultCharacterSet) { * passed to a callback. * * @param {object} callback The callback to update. - * @param {string} loadtype The loadtype property to add to the event. + * @param {object} info Info object to append to the event. * @returns {object} A function representing the modified callback. */ - function augmentCallbackEvent(callback, loadtype) { + function augmentCallbackEvent(callback, info) { return function (event) { - event.loadtype = loadtype; + var keys = Object.keys(info); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + event[key] = info[key]; + } callback(event); }; } @@ -235,14 +258,14 @@ dwv.LoadController = function (defaultCharacterSet) { * * @param {object} _event The load start event. */ -dwv.LoadController.prototype.onloadstart = function (_event) {}; +dwv.ctrl.LoadController.prototype.onloadstart = function (_event) {}; /** * Handle a load progress event. * Default does nothing. * * @param {object} _event The progress event. */ -dwv.LoadController.prototype.onprogress = function (_event) {}; +dwv.ctrl.LoadController.prototype.onprogress = function (_event) {}; /** * Handle a load event. * Default does nothing. @@ -250,7 +273,7 @@ dwv.LoadController.prototype.onprogress = function (_event) {}; * @param {object} _event The load event fired * when a file has been loaded successfully. */ -dwv.LoadController.prototype.onload = function (_event) {}; +dwv.ctrl.LoadController.prototype.onload = function (_event) {}; /** * Handle a load end event. * Default does nothing. @@ -258,18 +281,18 @@ dwv.LoadController.prototype.onload = function (_event) {}; * @param {object} _event The load end event fired * when a file load has completed, successfully or not. */ -dwv.LoadController.prototype.onloadend = function (_event) {}; +dwv.ctrl.LoadController.prototype.onloadend = function (_event) {}; /** * Handle an error event. * Default does nothing. * * @param {object} _event The error event. */ -dwv.LoadController.prototype.onerror = function (_event) {}; +dwv.ctrl.LoadController.prototype.onerror = function (_event) {}; /** * Handle an abort event. * Default does nothing. * * @param {object} _event The abort event. */ -dwv.LoadController.prototype.onabort = function (_event) {}; +dwv.ctrl.LoadController.prototype.onabort = function (_event) {}; diff --git a/src/app/toolboxController.js b/src/app/toolboxController.js index 486360b42a..96f0283aef 100644 --- a/src/app/toolboxController.js +++ b/src/app/toolboxController.js @@ -1,5 +1,6 @@ // namespaces var dwv = dwv || {}; +dwv.ctrl = dwv.ctrl || {}; /** * Toolbox controller. @@ -7,35 +8,32 @@ var dwv = dwv || {}; * @param {Array} toolList The list of tool objects. * @class */ -dwv.ToolboxController = function (toolList) { +dwv.ctrl.ToolboxController = function (toolList) { /** - * Point converter function + * Selected tool. * + * @type {object} * @private */ - var displayToIndexConverter = null; + var selectedTool = null; /** - * Selected tool. + * Callback store to allow attach/detach. * - * @type {object} + * @type {Array} * @private */ - var selectedTool = null; + var callbackStore = []; /** * Initialise. - * - * @param {Function} converter The display to index converter. */ - this.init = function (converter) { + this.init = function () { for (var key in toolList) { toolList[key].init(); } - // TODO Would prefer to have this done in the addLayerListeners - displayToIndexConverter = converter; // keydown listener - window.addEventListener('keydown', onMouch, true); + window.addEventListener('keydown', getOnMouch('window', 'keydown'), true); }; /** @@ -150,12 +148,13 @@ dwv.ToolboxController = function (toolList) { * * @param {object} layer The layer to listen to. */ - this.attachLayer = function (layer) { - layer.activate(); + this.bindLayer = function (layer) { + layer.bindInteraction(); // interaction events var names = dwv.gui.interactionEventNames; for (var i = 0; i < names.length; ++i) { - layer.addEventListener(names[i], onMouch); + layer.addEventListener(names[i], + getOnMouch(layer.getId(), names[i])); } }; @@ -164,12 +163,13 @@ dwv.ToolboxController = function (toolList) { * * @param {object} layer The layer to stop listening to. */ - this.detachLayer = function (layer) { - layer.deactivate(); + this.unbindLayer = function (layer) { + layer.unbindInteraction(); // interaction events var names = dwv.gui.interactionEventNames; for (var i = 0; i < names.length; ++i) { - layer.removeEventListener(names[i], onMouch); + layer.removeEventListener(names[i], + getOnMouch(layer.getId(), names[i])); } }; @@ -178,71 +178,64 @@ dwv.ToolboxController = function (toolList) { * the mouse/touch position relative to the canvas element. * It then passes it to the current tool. * - * @param {object} event The event to handle. + * @param {string} layerId The layer id. + * @param {string} eventType The event type. + * @returns {object} A callback for the provided layer and event. * @private */ - function onMouch(event) { - // make sure we have a tool - if (!selectedTool) { - return; - } - - // flag not to get confused between touch and mouse - var handled = false; - // Store the event position relative to the image canvas - // in an extra member of the event: - // event._x and event._y. - var offsets = null; - var position = null; - if (event.type === 'touchstart' || - event.type === 'touchmove') { + function getOnMouch(layerId, eventType) { + // augment event with converted offsets + var augmentEventOffsets = function (event) { // event offset(s) - offsets = dwv.gui.getEventOffset(event); + var offsets = dwv.gui.getEventOffset(event); // should have at least one offset - event._xs = offsets[0].x; - event._ys = offsets[0].y; - position = displayToIndexConverter(offsets[0]); - event._x = parseInt(position.x, 10); - event._y = parseInt(position.y, 10); + event._x = offsets[0].x; + event._y = offsets[0].y; // possible second if (offsets.length === 2) { - event._x1s = offsets[1].x; - event._y1s = offsets[1].y; - position = displayToIndexConverter(offsets[1]); - event._x1 = parseInt(position.x, 10); - event._y1 = parseInt(position.y, 10); + event._x1 = offsets[1].x; + event._y1 = offsets[1].y; + } + }; + + var applySelectedTool = function (event) { + // make sure we have a tool + if (selectedTool) { + var func = selectedTool[event.type]; + if (func) { + func(event); + } } - // set handle event flag - handled = true; - } else if (event.type === 'mousemove' || - event.type === 'mousedown' || - event.type === 'mouseup' || - event.type === 'mouseout' || - event.type === 'wheel' || - event.type === 'dblclick') { - offsets = dwv.gui.getEventOffset(event); - event._xs = offsets[0].x; - event._ys = offsets[0].y; - position = displayToIndexConverter(offsets[0]); - event._x = parseInt(position.x, 10); - event._y = parseInt(position.y, 10); - // set handle event flag - handled = true; - } else if (event.type === 'keydown' || - event.type === 'touchend') { - handled = true; + }; + + if (typeof callbackStore[layerId] === 'undefined') { + callbackStore[layerId] = []; } - // Call the event handler of the curently selected tool. - if (handled) { - if (event.type !== 'keydown') { - event.preventDefault(); - } - var func = selectedTool[event.type]; - if (func) { - func(event); + if (typeof callbackStore[layerId][eventType] === 'undefined') { + var callback = null; + if (eventType === 'keydown') { + callback = function (event) { + applySelectedTool(event); + }; + } else if (eventType === 'touchend') { + callback = function (event) { + event.preventDefault(); + applySelectedTool(event); + }; + } else { + // mouse or touch events + callback = function (event) { + event.preventDefault(); + augmentEventOffsets(event); + applySelectedTool(event); + }; } + // store callback + callbackStore[layerId][eventType] = callback; } + + return callbackStore[layerId][eventType]; } -}; // class dwv.ToolboxController +}; // class ToolboxController diff --git a/src/app/viewController.js b/src/app/viewController.js index ede0ff6bb0..d3b4042f16 100644 --- a/src/app/viewController.js +++ b/src/app/viewController.js @@ -1,5 +1,6 @@ // namespaces var dwv = dwv || {}; +dwv.ctrl = dwv.ctrl || {}; /** * View controller. @@ -7,12 +8,27 @@ var dwv = dwv || {}; * @param {dwv.image.View} view The associated view. * @class */ -dwv.ViewController = function (view) { +dwv.ctrl.ViewController = function (view) { // closure to self var self = this; - // Slice/frame player ID (created by setInterval) + // third dimension player ID (created by setInterval) var playerID = null; + // setup the plane helper + var planeHelper = new dwv.image.PlaneHelper( + view.getImage().getGeometry().getSpacing(), + view.getOrientation() + ); + + /** + * Get the plane helper. + * + * @returns {object} The helper. + */ + this.getPlaneHelper = function () { + return planeHelper; + }; + /** * Initialise the controller. */ @@ -21,8 +37,6 @@ dwv.ViewController = function (view) { this.setWindowLevelPresetById(0); // default position this.setCurrentPosition2D(0, 0); - // default frame - this.setCurrentFrame(0); }; /** @@ -65,7 +79,7 @@ dwv.ViewController = function (view) { /** * Check if the controller is playing. * - * @returns {boolean} True is the controler is playing slices/frames. + * @returns {boolean} True if the controler is playing. */ this.isPlaying = function () { return (playerID !== null); @@ -74,12 +88,82 @@ dwv.ViewController = function (view) { /** * Get the current position. * - * @returns {object} The position. + * @returns {dwv.math.Point} The position. */ this.getCurrentPosition = function () { return view.getCurrentPosition(); }; + /** + * Get the current index. + * + * @returns {dwv.math.Index} The current index. + */ + this.getCurrentIndex = function () { + return view.getCurrentIndex(); + }; + + /** + * Get the current oriented position. + * + * @returns {dwv.math.Point} The position. + */ + this.getCurrentOrientedPosition = function () { + var res = view.getCurrentPosition(); + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + if (typeof view.getOrientation() !== 'undefined') { + res = view.getOrientation().getInverse().getAbs().multiplyVector3D(res); + } + return res; + }; + + /** + * Get the scroll index. + * + * @returns {number} The index. + */ + this.getScrollIndex = function () { + return view.getScrollIndex(); + }; + + /** + * Get the current scroll index value. + * + * @returns {object} The value. + */ + this.getCurrentScrollIndexValue = function () { + return view.getCurrentIndex().get(view.getScrollIndex()); + }; + + /** + * Get the current scroll position value. + * + * @returns {object} The value. + */ + this.getCurrentScrollPosition = function () { + var scrollIndex = view.getScrollIndex(); + return view.getCurrentPosition().get(scrollIndex); + }; + + /** + * Generate display image data to be given to a canvas. + * + * @param {Array} array The array to fill in. + */ + this.generateImageData = function (array) { + view.generateImageData(array); + }; + + /** + * Set the associated image. + * + * @param {Image} img The associated image. + */ + this.setImage = function (img) { + view.setImage(img); + }; + /** * Get the current spacing. * @@ -87,7 +171,7 @@ dwv.ViewController = function (view) { */ this.get2DSpacing = function () { var spacing = view.getImage().getGeometry().getSpacing(); - return [spacing.getColumnSpacing(), spacing.getRowSpacing()]; + return [spacing.get(0), spacing.get(1)]; }; /** @@ -98,12 +182,43 @@ dwv.ViewController = function (view) { * @returns {Array} A list of values. */ this.getImageRegionValues = function (min, max) { + var image = view.getImage(); + var orientation = view.getOrientation(); + var position = this.getCurrentIndex(); + var rescaled = true; + + // created oriented slice if needed + if (!dwv.math.isIdentityMat33(orientation)) { + // generate slice values + var sliceIter = dwv.image.getSliceIterator( + image, + position, + rescaled, + orientation + ); + var sliceValues = dwv.image.getIteratorValues(sliceIter); + // oriented geometry + var orientedSize = image.getGeometry().getSize(orientation); + var sizeValues = orientedSize.getValues(); + sizeValues[2] = 1; + var sliceSize = new dwv.image.Size(sizeValues); + var orientedSpacing = image.getGeometry().getSpacing(orientation); + var spacingValues = orientedSpacing.getValues(); + spacingValues[2] = 1; + var sliceSpacing = new dwv.image.Spacing(spacingValues); + var sliceOrigin = new dwv.math.Point3D(0, 0, 0); + var sliceGeometry = + new dwv.image.Geometry(sliceOrigin, sliceSize, sliceSpacing); + // slice image + image = new dwv.image.Image(sliceGeometry, sliceValues); + // update position + position = new dwv.math.Index([0, 0, 0]); + rescaled = false; + } + + // get region values var iter = dwv.image.getRegionSliceIterator( - view.getImage(), - this.getCurrentPosition().k, - this.getCurrentFrame(), - true, min, max - ); + image, position, rescaled, min, max); var values = []; if (iter) { values = dwv.image.getIteratorValues(iter); @@ -120,8 +235,7 @@ dwv.ViewController = function (view) { this.getImageVariableRegionValues = function (regions) { var iter = dwv.image.getVariableRegionSliceIterator( view.getImage(), - this.getCurrentPosition().k, - this.getCurrentFrame(), + this.getCurrentIndex(), true, regions ); var values = []; @@ -137,33 +251,41 @@ dwv.ViewController = function (view) { * @returns {boolean} True if possible. */ this.canQuantifyImage = function () { - return view.getImage().getNumberOfComponents() === 1; + return view.getImage().canQuantify(); }; /** * Can window and level be applied to the data? * - * @returns {boolean} True if the data is monochrome. + * @returns {boolean} True if possible. */ this.canWindowLevel = function () { - return view.getImage().getPhotometricInterpretation() - .match(/MONOCHROME/) !== null; + return view.getImage().canWindowLevel(); }; /** - * Is the data mono-frame? + * Can the data be scrolled? * - * @returns {boolean} True if the data only contains one frame. + * @returns {boolean} True if the data has a third dimension greater than one. */ - this.isMonoFrameData = function () { - return view.getImage().getNumberOfFrames() === 1; + this.canScroll = function () { + return view.getImage().canScroll(view.getOrientation()); + }; + + /** + * Get the image size. + * + * @returns {dwv.image.Size} The size. + */ + this.getImageSize = function () { + return view.getImage().getGeometry().getSize(); }; /** * Set the current position. * - * @param {object} pos The position. - * @param {boolean} silent If true, does not fire a slicechange event. + * @param {dwv.math.Point} pos The position. + * @param {boolean} silent If true, does not fire a positionchange event. * @returns {boolean} False if not in bounds. */ this.setCurrentPosition = function (pos, silent) { @@ -171,126 +293,145 @@ dwv.ViewController = function (view) { }; /** - * Set the current 2D (i,j) position. + * Set the current 2D (x,y) position. * - * @param {number} i The column index. - * @param {number} j The row index. + * @param {number} x The column position. + * @param {number} y The row position. * @returns {boolean} False if not in bounds. */ - this.setCurrentPosition2D = function (i, j) { - return view.setCurrentPosition({ - i: i, - j: j, - k: view.getCurrentPosition().k - }); + this.setCurrentPosition2D = function (x, y) { + return view.setCurrentPosition( + this.getPositionFromPlanePoint({x: x, y: y})); }; /** - * Set the current slice position. + * Set the current index. * - * @param {number} k The slice index. + * @param {dwv.math.Index} index The index. + * @param {boolean} silent If true, does not fire a positionchange event. * @returns {boolean} False if not in bounds. */ - this.setCurrentSlice = function (k) { - return view.setCurrentPosition({ - i: view.getCurrentPosition().i, - j: view.getCurrentPosition().j, - k: k - }); + this.setCurrentIndex = function (index, silent) { + return view.setCurrentIndex(index, silent); }; /** - * Increment the current slice number. + * Get a 3D position from a plane 2D position. * - * @returns {boolean} False if not in bounds. + * @param {dwv.math.Point2D} point2D The 2D position as {x,y}. + * @returns {dwv.math.Point} The 3D point. */ - this.incrementSliceNb = function () { - return self.setCurrentSlice(view.getCurrentPosition().k + 1); + this.getPositionFromPlanePoint = function (point2D) { + // keep third direction + var k = this.getCurrentScrollIndexValue(); + var planePoint = new dwv.math.Point3D(point2D.x, point2D.y, k); + // de-orient + var point = planeHelper.getDeOrientedVector3D(planePoint); + // ~indexToWorld to not loose precision + var geometry = view.getImage().getGeometry(); + var point3D = geometry.pointToWorld(point); + // merge with current position to keep extra dimensions + return this.getCurrentPosition().mergeWith3D(point3D); }; /** - * Decrement the current slice number. + * Get a plane 3D position from a plane 2D position: does not compensate + * for the image origin. Needed for setting the scale center... * - * @returns {boolean} False if not in bounds. + * @param {dwv.math.Point2D} point2D The 2D position as {x,y}. + * @returns {dwv.math.Point3D} The 3D point. */ - this.decrementSliceNb = function () { - return self.setCurrentSlice(view.getCurrentPosition().k - 1); + this.getPlanePositionFromPlanePoint = function (point2D) { + // keep third direction + var k = this.getCurrentScrollIndexValue(); + var planePoint = new dwv.math.Point3D(point2D.x, point2D.y, k); + // de-orient + var point = planeHelper.getDeOrientedVector3D(planePoint); + // ~indexToWorld to not loose precision + var geometry = view.getImage().getGeometry(); + var spacing = geometry.getSpacing(); + return new dwv.math.Point3D( + point.getX() * spacing.get(0), + point.getY() * spacing.get(1), + point.getZ() * spacing.get(2)); }; /** - * Get the current frame. + * Get a 3D offset from a plane one. * - * @returns {number} The frame number. + * @param {object} offset2D The plane offset as {x,y}. + * @returns {dwv.math.Vector3D} The 3D world offset. */ - this.getCurrentFrame = function () { - return view.getCurrentFrame(); + this.getOffset3DFromPlaneOffset = function (offset2D) { + return planeHelper.getOffset3DFromPlaneOffset(offset2D); }; /** - * Set the current frame. + * Increment the provided dimension. * - * @param {number} number The frame number. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. * @returns {boolean} False if not in bounds. */ - this.setCurrentFrame = function (number) { - return view.setCurrentFrame(number); + this.incrementIndex = function (dim, silent) { + return view.incrementIndex(dim, silent); }; /** - * Increment the current frame. + * Decrement the provided dimension. * + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. * @returns {boolean} False if not in bounds. */ - this.incrementFrameNb = function () { - return view.setCurrentFrame(view.getCurrentFrame() + 1); + this.decrementIndex = function (dim, silent) { + return view.decrementIndex(dim, silent); }; /** - * Decrement the current frame. + * Decrement the scroll dimension index. * + * @param {boolean} silent Do not send event. * @returns {boolean} False if not in bounds. */ - this.decrementFrameNb = function () { - return view.setCurrentFrame(view.getCurrentFrame() - 1); + this.decrementScrollIndex = function (silent) { + return view.decrementScrollIndex(silent); }; /** - * Go to first slice . + * Increment the scroll dimension index. * + * @param {boolean} silent Do not send event. * @returns {boolean} False if not in bounds. - * @deprecated Use the setCurrentSlice function. */ - this.goFirstSlice = function () { - return view.setCurrentPosition({ - i: view.getCurrentPosition().i, - j: view.getCurrentPosition().j, - k: 0 - }); + this.incrementScrollIndex = function (silent) { + return view.incrementScrollIndex(silent); }; /** - * + * Scroll play: loop through all slices. */ this.play = function () { + if (!this.canScroll()) { + return; + } if (playerID === null) { - var nSlices = view.getImage().getGeometry().getSize().getNumberOfSlices(); - var nFrames = view.getImage().getNumberOfFrames(); var recommendedDisplayFrameRate = view.getImage().getMeta().RecommendedDisplayFrameRate; var milliseconds = view.getPlaybackMilliseconds( recommendedDisplayFrameRate); playerID = setInterval(function () { - if (nSlices !== 1) { - if (!self.incrementSliceNb()) { - self.setCurrentSlice(0); - } - } else if (nFrames !== 1) { - if (!self.incrementFrameNb()) { - self.setCurrentFrame(0); - } + // end of scroll, loop back + if (!self.incrementScrollIndex()) { + var pos1 = self.getCurrentIndex(); + var values = pos1.getValues(); + var orientation = view.getOrientation(); + values[orientation.getThirdColMajorDirection()] = 0; + var index = new dwv.math.Index(values); + var geometry = view.getImage().getGeometry(); + self.setCurrentPosition(geometry.indexToWorld(index)); } - }, milliseconds); } else { this.stop(); @@ -298,7 +439,7 @@ dwv.ViewController = function (view) { }; /** - * + * Stop scroll playing. */ this.stop = function () { if (playerID !== null) { @@ -347,6 +488,15 @@ dwv.ViewController = function (view) { view.setColourMap(colourMap); }; + /** + * Set the view per value alpha function. + * + * @param {Function} func The function. + */ + this.setViewAlphaFunction = function (func) { + view.setAlphaFunction(func); + }; + /** * Set the colour map from a name. * @@ -361,4 +511,4 @@ dwv.ViewController = function (view) { this.setColourMap(dwv.tool.colourMaps[name]); }; -}; // class dwv.ViewController +}; // class ViewController diff --git a/src/dicom/dataReader.js b/src/dicom/dataReader.js new file mode 100644 index 0000000000..163f5b6c94 --- /dev/null +++ b/src/dicom/dataReader.js @@ -0,0 +1,389 @@ +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + +/** + * Is the Native endianness Little Endian. + * + * @type {boolean} + */ +dwv.dicom.isNativeLittleEndian = function () { + return new Int8Array(new Int16Array([1]).buffer)[0] > 0; +}; + +/** + * Flip an array's endianness. + * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. + * + * @param {object} array The array to flip (modified). + */ +dwv.dicom.flipArrayEndianness = function (array) { + var blen = array.byteLength; + var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); + var bpe = array.BYTES_PER_ELEMENT; + var tmp; + for (var i = 0; i < blen; i += bpe) { + for (var j = i + bpe - 1, k = i; j > k; j--, k++) { + tmp = u8[k]; + u8[k] = u8[j]; + u8[j] = tmp; + } + } +}; + +/** + * Data reader. + * + * @class + * @param {Array} buffer The input array buffer. + * @param {boolean} isLittleEndian Flag to tell if the data is little + * or big endian. + */ +dwv.dicom.DataReader = function (buffer, isLittleEndian) { + // Set endian flag if not defined. + if (typeof isLittleEndian === 'undefined') { + isLittleEndian = true; + } + + // Default text decoder + var defaultTextDecoder = {}; + defaultTextDecoder.decode = function (buffer) { + var result = ''; + for (var i = 0, leni = buffer.length; i < leni; ++i) { + result += String.fromCharCode(buffer[i]); + } + return result; + }; + + // Text decoder + var textDecoder = defaultTextDecoder; + if (typeof window.TextDecoder !== 'undefined') { + textDecoder = new TextDecoder('iso-8859-1'); + } + + /** + * Set the utfLabel used to construct the TextDecoder. + * + * @param {string} label The encoding label. + */ + this.setUtfLabel = function (label) { + if (typeof window.TextDecoder !== 'undefined') { + textDecoder = new TextDecoder(label); + } + }; + + /** + * Is the Native endianness Little Endian. + * + * @private + * @type {boolean} + */ + var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); + + /** + * Flag to know if the TypedArray data needs flipping. + * + * @private + * @type {boolean} + */ + var needFlip = (isLittleEndian !== isNativeLittleEndian); + + /** + * The main data view. + * + * @private + * @type {DataView} + */ + var view = new DataView(buffer); + + /** + * Read Uint16 (2 bytes) data. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. + */ + this.readUint16 = function (byteOffset) { + return view.getUint16(byteOffset, isLittleEndian); + }; + + /** + * Read Uint32 (4 bytes) data. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. + */ + this.readUint32 = function (byteOffset) { + return view.getUint32(byteOffset, isLittleEndian); + }; + + /** + * Read Int32 (4 bytes) data. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. + */ + this.readInt32 = function (byteOffset) { + return view.getInt32(byteOffset, isLittleEndian); + }; + + /** + * Read Float32 (4 bytes) data. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. + */ + this.readFloat32 = function (byteOffset) { + return view.getFloat32(byteOffset, isLittleEndian); + }; + + /** + * Read Float64 (8 bytes) data. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {number} The read data. + */ + this.readFloat64 = function (byteOffset) { + return view.getFloat64(byteOffset, isLittleEndian); + }; + + /** + * Read binary (0/1) array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readBinaryArray = function (byteOffset, size) { + // input + var bitArray = new Uint8Array(buffer, byteOffset, size); + // result + var byteArrayLength = 8 * bitArray.length; + var data = new Uint8Array(byteArrayLength); + var bitNumber = 0; + var bitIndex = 0; + for (var i = 0; i < byteArrayLength; ++i) { + bitNumber = i % 8; + bitIndex = Math.floor(i / 8); + // see https://stackoverflow.com/questions/4854207/get-a-specific-bit-from-byte/4854257 + data[i] = 255 * ((bitArray[bitIndex] & (1 << bitNumber)) !== 0); + } + return data; + }; + + /** + * Read Uint8 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readUint8Array = function (byteOffset, size) { + return new Uint8Array(buffer, byteOffset, size); + }; + + /** + * Read Int8 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readInt8Array = function (byteOffset, size) { + return new Int8Array(buffer, byteOffset, size); + }; + + /** + * Read Uint16 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readUint16Array = function (byteOffset, size) { + var bpe = Uint16Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) + if (byteOffset % bpe === 0) { + data = new Uint16Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Uint16Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readUint16(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read Int16 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readInt16Array = function (byteOffset, size) { + var bpe = Int16Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) + if (byteOffset % bpe === 0) { + data = new Int16Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Int16Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readInt16(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read Uint32 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readUint32Array = function (byteOffset, size) { + var bpe = Uint32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Uint32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Uint32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readUint32(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read Int32 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readInt32Array = function (byteOffset, size) { + var bpe = Int32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Int32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Int32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readInt32(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read Float32 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readFloat32Array = function (byteOffset, size) { + var bpe = Float32Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) + if (byteOffset % bpe === 0) { + data = new Float32Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Float32Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readFloat32(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read Float64 array. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} size The size of the array. + * @returns {Array} The read data. + */ + this.readFloat64Array = function (byteOffset, size) { + var bpe = Float64Array.BYTES_PER_ELEMENT; + var arraySize = size / bpe; + var data = null; + // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) + if (byteOffset % bpe === 0) { + data = new Float64Array(buffer, byteOffset, arraySize); + if (needFlip) { + dwv.dicom.flipArrayEndianness(data); + } + } else { + data = new Float64Array(arraySize); + for (var i = 0; i < arraySize; ++i) { + data[i] = this.readFloat64(byteOffset + bpe * i); + } + } + return data; + }; + + /** + * Read data as a string. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} nChars The number of characters to read. + * @returns {string} The read data. + */ + this.readString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return defaultTextDecoder.decode(data); + }; + + /** + * Read data as a 'special' string, decoding it if the + * TextDecoder is available. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} nChars The number of characters to read. + * @returns {string} The read data. + */ + this.readSpecialString = function (byteOffset, nChars) { + var data = this.readUint8Array(byteOffset, nChars); + return textDecoder.decode(data); + }; + +}; // class DataReader + +/** + * Read data as an hexadecimal string. + * + * @param {number} byteOffset The offset to start reading from. + * @returns {Array} The read data. + */ +dwv.dicom.DataReader.prototype.readHex = function (byteOffset) { + // read and convert to hex string + var str = this.readUint16(byteOffset).toString(16); + // return padded + return '0x0000'.substr(0, 6 - str.length) + str.toUpperCase(); +}; diff --git a/src/dicom/dataWriter.js b/src/dicom/dataWriter.js new file mode 100644 index 0000000000..aa4b9e86d3 --- /dev/null +++ b/src/dicom/dataWriter.js @@ -0,0 +1,326 @@ +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + +/** + * Data writer. + * + * @class + * @param {Array} buffer The input array buffer. + * @param {boolean} isLittleEndian Flag to tell if the data is + * little or big endian. + */ +dwv.dicom.DataWriter = function (buffer, isLittleEndian) { + // Set endian flag if not defined. + if (typeof isLittleEndian === 'undefined') { + isLittleEndian = true; + } + + // Default text encoder + var defaultTextEncoder = {}; + defaultTextEncoder.encode = function (buffer) { + var result = new Uint8Array(buffer.length); + for (var i = 0, leni = buffer.length; i < leni; ++i) { + result[i] = buffer.charCodeAt(i); + } + return result; + }; + + // Text encoder + var textEncoder = defaultTextEncoder; + if (typeof window.TextEncoder !== 'undefined') { + textEncoder = new TextEncoder('iso-8859-1'); + } + + /** + * Set the utfLabel used to construct the TextEncoder. + * + * @param {string} label The encoding label. + */ + this.setUtfLabel = function (label) { + if (typeof window.TextEncoder !== 'undefined') { + textEncoder = new TextEncoder(label); + } + }; + + // private DataView + var view = new DataView(buffer); + + // flag to use VR=UN for private sequences, default to false + // (mainly used in tests) + this.useUnVrForPrivateSq = false; + + /** + * Write Uint8 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeUint8 = function (byteOffset, value) { + view.setUint8(byteOffset, value); + return byteOffset + Uint8Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int8 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeInt8 = function (byteOffset, value) { + view.setInt8(byteOffset, value); + return byteOffset + Int8Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Uint16 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeUint16 = function (byteOffset, value) { + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int16 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeInt16 = function (byteOffset, value) { + view.setInt16(byteOffset, value, isLittleEndian); + return byteOffset + Int16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Uint32 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeUint32 = function (byteOffset, value) { + view.setUint32(byteOffset, value, isLittleEndian); + return byteOffset + Uint32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Int32 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeInt32 = function (byteOffset, value) { + view.setInt32(byteOffset, value, isLittleEndian); + return byteOffset + Int32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Float32 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeFloat32 = function (byteOffset, value) { + view.setFloat32(byteOffset, value, isLittleEndian); + return byteOffset + Float32Array.BYTES_PER_ELEMENT; + }; + + /** + * Write Float64 data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} value The data to write. + * @returns {number} The new offset position. + */ + this.writeFloat64 = function (byteOffset, value) { + view.setFloat64(byteOffset, value, isLittleEndian); + return byteOffset + Float64Array.BYTES_PER_ELEMENT; + }; + + /** + * Write string data as hexadecimal. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} str The padded hexadecimal string to write ('0x####'). + * @returns {number} The new offset position. + */ + this.writeHex = function (byteOffset, str) { + // remove first two chars and parse + var value = parseInt(str.substr(2), 16); + view.setUint16(byteOffset, value, isLittleEndian); + return byteOffset + Uint16Array.BYTES_PER_ELEMENT; + }; + + /** + * Write string data. + * + * @param {number} byteOffset The offset to start writing from. + * @param {number} str The data to write. + * @returns {number} The new offset position. + */ + this.writeString = function (byteOffset, str) { + var data = defaultTextEncoder.encode(str); + return this.writeUint8Array(byteOffset, data); + }; + + /** + * Write data as a 'special' string, encoding it if the + * TextEncoder is available. + * + * @param {number} byteOffset The offset to start reading from. + * @param {number} str The data to write. + * @returns {number} The new offset position. + */ + this.writeSpecialString = function (byteOffset, str) { + var data = textEncoder.encode(str); + return this.writeUint8Array(byteOffset, data); + }; + +}; // class DataWriter + +/** + * Write a boolean array as binary. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeBinaryArray = function (byteOffset, array) { + if (array.length % 8 !== 0) { + throw new Error('Cannot write boolean array as binary.'); + } + var byte = null; + var val = null; + for (var i = 0, len = array.length; i < len; i += 8) { + byte = 0; + for (var j = 0; j < 8; ++j) { + val = array[i + j] === 0 ? 0 : 1; + byte += val << j; + } + byteOffset = this.writeUint8(byteOffset, byte); + } + return byteOffset; +}; + +/** + * Write Uint8 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint8Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint8(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int8 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt8Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt8(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Uint16 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint16Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint16(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int16 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt16Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt16(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Uint32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeUint32Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeUint32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Int32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeInt32Array = function (byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeInt32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Float32 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat32Array = function ( + byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeFloat32(byteOffset, array[i]); + } + return byteOffset; +}; + +/** + * Write Float64 array. + * + * @param {number} byteOffset The offset to start writing from. + * @param {Array} array The array to write. + * @returns {number} The new offset position. + */ +dwv.dicom.DataWriter.prototype.writeFloat64Array = function ( + byteOffset, array) { + for (var i = 0, len = array.length; i < len; ++i) { + byteOffset = this.writeFloat64(byteOffset, array[i]); + } + return byteOffset; +}; diff --git a/src/dicom/dicomElementsWrapper.js b/src/dicom/dicomElementsWrapper.js index 919dba5c1d..bb61c78c8c 100644 --- a/src/dicom/dicomElementsWrapper.js +++ b/src/dicom/dicomElementsWrapper.js @@ -69,17 +69,10 @@ dwv.dicom.DicomElementsWrapper = function (dicomElements) { * @returns {string} The tag name. */ this.getTagName = function (tag) { - var dict = dwv.dicom.dictionary; - // dictionnary entry - var dictElement = null; - if (typeof dict[tag.group] !== 'undefined' && - typeof dict[tag.group][tag.element] !== 'undefined') { - dictElement = dict[tag.group][tag.element]; - } - // name - var name = 'Unknown Tag & Data'; - if (dictElement !== null) { - name = dictElement[2]; + var tagObj = new dwv.dicom.Tag(tag.group, tag.element); + var name = tagObj.getNameFromDictionary(); + if (name === null) { + name = tagObj.getKey2(); } return name; }; @@ -192,18 +185,18 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( // Polyfill for Number.isInteger. var isInteger = Number.isInteger || function (value) { return typeof value === 'number' && - isFinite(value) && - Math.floor(value) === value; + isFinite(value) && + Math.floor(value) === value; }; // TODO Support sequences. if (dicomElement.vr !== 'SQ' && - dicomElement.value.length === 1 && dicomElement.value[0] === '') { + dicomElement.value.length === 1 && dicomElement.value[0] === '') { str += '(no value available)'; } else if (dicomElement.tag.group === '0x7FE0' && - dicomElement.tag.element === '0x0010' && - dicomElement.vl === 'u/l') { + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l') { str = '(PixelSequence)'; } else if (dicomElement.vr === 'DA' && pretty) { var daValue = dicomElement.value[0]; @@ -228,10 +221,13 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementValueAsString = function ( var tmSeconds = tmValue.length >= 6 ? tmValue.substr(4, 2) : '00'; str = tmHour + ':' + tmMinute + ':' + tmSeconds; } else { - var isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + var isOtherVR = false; + if (dicomElement.vr.length !== 0) { + isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + } var isFloatNumberVR = (dicomElement.vr === 'FL' || - dicomElement.vr === 'FD' || - dicomElement.vr === 'DS'); + dicomElement.vr === 'FD' || + dicomElement.vr === 'DS'); var valueStr = ''; for (var k = 0, lenk = dicomElement.value.length; k < lenk; ++k) { valueStr = ''; @@ -297,30 +293,29 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( // default prefix prefix = prefix || ''; - // get element from dictionary - var dict = dwv.dicom.dictionary; - var dictElement = null; - if (typeof dict[dicomElement.tag.group] !== 'undefined' && - typeof dict[dicomElement.tag.group][dicomElement.tag.element] !== - 'undefined') { - dictElement = dict[dicomElement.tag.group][dicomElement.tag.element]; - } + // get tag anme from dictionary + var tag = new dwv.dicom.Tag( + dicomElement.tag.group, dicomElement.tag.element); + var tagName = tag.getNameFromDictionary(); var deSize = dicomElement.value.length; - var isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + var isOtherVR = false; + if (dicomElement.vr.length !== 0) { + isOtherVR = (dicomElement.vr[0].toUpperCase() === 'O'); + } // no size for delimitations if (dicomElement.tag.group === '0xFFFE' && ( dicomElement.tag.element === '0xE00D' || - dicomElement.tag.element === '0xE0DD')) { + dicomElement.tag.element === '0xE0DD')) { deSize = 0; } else if (isOtherVR) { deSize = 1; } var isPixSequence = (dicomElement.tag.group === '0x7FE0' && - dicomElement.tag.element === '0x0010' && - dicomElement.vl === 'u/l'); + dicomElement.tag.element === '0x0010' && + dicomElement.vl === 'u/l'); var line = null; @@ -394,8 +389,8 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( line += ', '; line += deSize; //dictElement[1]; line += ' '; - if (dictElement !== null) { - line += dictElement[2]; + if (tagName !== null) { + line += tagName; } else { line += 'Unknown Tag & Data'; } @@ -495,8 +490,7 @@ dwv.dicom.DicomElementsWrapper.prototype.getElementAsString = function ( */ dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( group, element) { - return this.getFromKey( - dwv.dicom.getGroupElementKey(group, element)); + return this.getFromKey(new dwv.dicom.Tag(group, element).getKey()); }; /** @@ -508,16 +502,53 @@ dwv.dicom.DicomElementsWrapper.prototype.getFromGroupElement = function ( */ dwv.dicom.DicomElementsWrapper.prototype.getFromName = function (name) { var value = null; - var tagGE = dwv.dicom.getGroupElementFromName(name); + var tag = dwv.dicom.getTagFromDictionary(name); // check that we are not at the end of the dictionary - if (tagGE.group !== null && tagGE.element !== null) { - value = this.getFromKey( - dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element) - ); + if (tag !== null) { + value = this.getFromKey(tag.getKey()); } return value; }; +/** + * Get the pixel spacing from the different spacing tags. + * + * @returns {object} The read spacing or the default [1,1]. + */ +dwv.dicom.DicomElementsWrapper.prototype.getPixelSpacing = function () { + // default + var rowSpacing = 1; + var columnSpacing = 1; + + // 1. PixelSpacing + // 2. ImagerPixelSpacing + // 3. NominalScannedPixelSpacing + // 4. PixelAspectRatio + var keys = ['x00280030', 'x00181164', 'x00182010', 'x00280034']; + for (var k = 0; k < keys.length; ++k) { + var spacing = this.getFromKey(keys[k], true); + if (spacing && spacing.length === 2) { + rowSpacing = parseFloat(spacing[0]); + columnSpacing = parseFloat(spacing[1]); + break; + } + } + + // check + if (columnSpacing === 0) { + dwv.logger.warn('Zero column spacing.'); + columnSpacing = 1; + } + if (rowSpacing === 0) { + dwv.logger.warn('Zero row spacing.'); + rowSpacing = 1; + } + + // return + // (slice spacing will be calculated using the image position patient) + return new dwv.image.Spacing([columnSpacing, rowSpacing, 1]); +}; + /** * Get the file list from a DICOMDIR * @@ -533,7 +564,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { // Directory Record Sequence if (typeof elements.x00041220 === 'undefined' || - typeof elements.x00041220.value === 'undefined') { + typeof elements.x00041220.value === 'undefined') { dwv.logger.warn('No Directory Record Sequence found in DICOMDIR.'); return; } @@ -550,7 +581,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { for (var i = 0; i < dirSeq.length; ++i) { // Directory Record Type if (typeof dirSeq[i].x00041430 === 'undefined' || - typeof dirSeq[i].x00041430.value === 'undefined') { + typeof dirSeq[i].x00041430.value === 'undefined') { continue; } var recType = dwv.dicom.cleanString(dirSeq[i].x00041430.value[0]); @@ -565,7 +596,7 @@ dwv.dicom.getFileListFromDicomDir = function (data) { } else if (recType === 'IMAGE') { // Referenced File ID if (typeof dirSeq[i].x00041500 === 'undefined' || - typeof dirSeq[i].x00041500.value === 'undefined') { + typeof dirSeq[i].x00041500.value === 'undefined') { continue; } var refFileIds = dirSeq[i].x00041500.value; diff --git a/src/dicom/dicomParser.js b/src/dicom/dicomParser.js index 1425b18cba..04dad08f01 100755 --- a/src/dicom/dicomParser.js +++ b/src/dicom/dicomParser.js @@ -9,7 +9,23 @@ dwv.dicom = dwv.dicom || {}; * @returns {string} The version of the library. */ dwv.getVersion = function () { - return '0.29.1'; + return '0.30.0'; +}; + +/** + * Check that an input buffer includes the DICOM prefix 'DICM' + * after the 128 bytes preamble. + * Ref: [DICOM File Meta]{@link https://dicom.nema.org/dicom/2013/output/chtml/part10/chapter_7.html#sect_7.1} + * + * @param {ArrayBuffer} buffer The buffer to check. + * @returns {boolean} True if the buffer includes the prefix. + */ +dwv.dicom.hasDicomPrefix = function (buffer) { + var prefixArray = new Uint8Array(buffer, 128, 4); + var stringReducer = function (previous, current) { + return previous += String.fromCharCode(current); + }; + return prefixArray.reduce(stringReducer, '') === 'DICM'; }; /** @@ -31,28 +47,6 @@ dwv.dicom.cleanString = function (inputStr) { return res; }; -/** - * Is the tag group a private tag group ? - * see: http://dicom.nema.org/medical/dicom/2015a/output/html/part05.html#sect_7.8 - * - * @param {string} group The group string as '0x####' - * @returns {boolean} True if the tag group is private, - * ie if its group is an odd number. - */ -dwv.dicom.isPrivateGroup = function (group) { - var groupNumber = parseInt(group.substr(2, 6), 10); - return (groupNumber % 2) === 1; -}; - -/** - * Is the Native endianness Little Endian. - * - * @type {boolean} - */ -dwv.dicom.isNativeLittleEndian = function () { - return new Int8Array(new Int16Array([1]).buffer)[0] > 0; -}; - /** * Get the utfLabel (used by the TextDecoder) from a character set term * References: @@ -107,479 +101,6 @@ dwv.dicom.getUtfLabel = function (charSetTerm) { return label; }; -/** - * Data reader. - * - * @class - * @param {Array} buffer The input array buffer. - * @param {boolean} isLittleEndian Flag to tell if the data is little - * or big endian. - */ -dwv.dicom.DataReader = function (buffer, isLittleEndian) { - // Set endian flag if not defined. - if (typeof isLittleEndian === 'undefined') { - isLittleEndian = true; - } - - // Default text decoder - var defaultTextDecoder = {}; - defaultTextDecoder.decode = function (buffer) { - var result = ''; - for (var i = 0, leni = buffer.length; i < leni; ++i) { - result += String.fromCharCode(buffer[i]); - } - return result; - }; - // Text decoder - var textDecoder = defaultTextDecoder; - if (typeof window.TextDecoder !== 'undefined') { - textDecoder = new TextDecoder('iso-8859-1'); - } - - /** - * Set the utfLabel used to construct the TextDecoder. - * - * @param {string} label The encoding label. - */ - this.setUtfLabel = function (label) { - if (typeof window.TextDecoder !== 'undefined') { - textDecoder = new TextDecoder(label); - } - }; - - /** - * Is the Native endianness Little Endian. - * - * @private - * @type {boolean} - */ - var isNativeLittleEndian = dwv.dicom.isNativeLittleEndian(); - - /** - * Flag to know if the TypedArray data needs flipping. - * - * @private - * @type {boolean} - */ - var needFlip = (isLittleEndian !== isNativeLittleEndian); - - /** - * The main data view. - * - * @private - * @type {DataView} - */ - var view = new DataView(buffer); - - /** - * Flip an array's endianness. - * Inspired from [DataStream.js]{@link https://github.com/kig/DataStream.js}. - * - * @param {object} array The array to flip (modified). - */ - this.flipArrayEndianness = function (array) { - var blen = array.byteLength; - var u8 = new Uint8Array(array.buffer, array.byteOffset, blen); - var bpel = array.BYTES_PER_ELEMENT; - var tmp; - for (var i = 0; i < blen; i += bpel) { - for (var j = i + bpel - 1, k = i; j > k; j--, k++) { - tmp = u8[k]; - u8[k] = u8[j]; - u8[j] = tmp; - } - } - }; - - /** - * Read Uint16 (2 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readUint16 = function (byteOffset) { - return view.getUint16(byteOffset, isLittleEndian); - }; - /** - * Read Uint32 (4 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readUint32 = function (byteOffset) { - return view.getUint32(byteOffset, isLittleEndian); - }; - /** - * Read Int32 (4 bytes) data. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {number} The read data. - */ - this.readInt32 = function (byteOffset) { - return view.getInt32(byteOffset, isLittleEndian); - }; - /** - * Read binary (0/1) array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readBinaryArray = function (byteOffset, size) { - // input - var bitArray = new Uint8Array(buffer, byteOffset, size); - // result - var byteArrayLength = 8 * bitArray.length; - var data = new Uint8Array(byteArrayLength); - var bitNumber = 0; - var bitIndex = 0; - for (var i = 0; i < byteArrayLength; ++i) { - bitNumber = i % 8; - bitIndex = Math.floor(i / 8); - // see https://stackoverflow.com/questions/4854207/get-a-specific-bit-from-byte/4854257 - data[i] = 255 * ((bitArray[bitIndex] & (1 << bitNumber)) !== 0); - } - return data; - }; - /** - * Read Uint8 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint8Array = function (byteOffset, size) { - return new Uint8Array(buffer, byteOffset, size); - }; - /** - * Read Int8 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt8Array = function (byteOffset, size) { - return new Int8Array(buffer, byteOffset, size); - }; - /** - * Read Uint16 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint16Array = function (byteOffset, size) { - var arraySize = size / Uint16Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Uint16Array.BYTES_PER_ELEMENT (=2) - if ((byteOffset % Uint16Array.BYTES_PER_ELEMENT) === 0) { - data = new Uint16Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Uint16Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getUint16((byteOffset + - Uint16Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Int16 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt16Array = function (byteOffset, size) { - var arraySize = size / Int16Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Int16Array.BYTES_PER_ELEMENT (=2) - if ((byteOffset % Int16Array.BYTES_PER_ELEMENT) === 0) { - data = new Int16Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Int16Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getInt16((byteOffset + - Int16Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Uint32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readUint32Array = function (byteOffset, size) { - var arraySize = size / Uint32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Uint32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Uint32Array.BYTES_PER_ELEMENT) === 0) { - data = new Uint32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Uint32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getUint32((byteOffset + - Uint32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Int32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readInt32Array = function (byteOffset, size) { - var arraySize = size / Int32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Int32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Int32Array.BYTES_PER_ELEMENT) === 0) { - data = new Int32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Int32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getInt32((byteOffset + - Int32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Float32 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readFloat32Array = function (byteOffset, size) { - var arraySize = size / Float32Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Float32Array.BYTES_PER_ELEMENT (=4) - if ((byteOffset % Float32Array.BYTES_PER_ELEMENT) === 0) { - data = new Float32Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Float32Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getFloat32((byteOffset + - Float32Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read Float64 array. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} size The size of the array. - * @returns {Array} The read data. - */ - this.readFloat64Array = function (byteOffset, size) { - var arraySize = size / Float64Array.BYTES_PER_ELEMENT; - var data = null; - // byteOffset should be a multiple of Float64Array.BYTES_PER_ELEMENT (=8) - if ((byteOffset % Float64Array.BYTES_PER_ELEMENT) === 0) { - data = new Float64Array(buffer, byteOffset, arraySize); - if (needFlip) { - this.flipArrayEndianness(data); - } - } else { - data = new Float64Array(arraySize); - for (var i = 0; i < arraySize; ++i) { - data[i] = view.getFloat64((byteOffset + - Float64Array.BYTES_PER_ELEMENT * i), - isLittleEndian); - } - } - return data; - }; - /** - * Read data as an hexadecimal string. - * - * @param {number} byteOffset The offset to start reading from. - * @returns {Array} The read data. - */ - this.readHex = function (byteOffset) { - // read and convert to hex string - var str = this.readUint16(byteOffset).toString(16); - // return padded - return '0x0000'.substr(0, 6 - str.length) + str.toUpperCase(); - }; - - /** - * Read data as a string. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} nChars The number of characters to read. - * @returns {string} The read data. - */ - this.readString = function (byteOffset, nChars) { - var data = this.readUint8Array(byteOffset, nChars); - return defaultTextDecoder.decode(data); - }; - - /** - * Read data as a 'special' string, decoding it if the - * TextDecoder is available. - * - * @param {number} byteOffset The offset to start reading from. - * @param {number} nChars The number of characters to read. - * @returns {string} The read data. - */ - this.readSpecialString = function (byteOffset, nChars) { - var data = this.readUint8Array(byteOffset, nChars); - return textDecoder.decode(data); - }; - -}; - -/** - * Get the group-element pair from a tag string name. - * - * @param {string} tagName The tag string name. - * @returns {object} group-element pair. - */ -dwv.dicom.getGroupElementFromName = function (tagName) { - var group = null; - var element = null; - var dict = dwv.dicom.dictionary; - var keys0 = Object.keys(dict); - var keys1 = null; - // label for nested loop break - outLabel: - // search through dictionary - for (var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) { - group = keys0[k0]; - keys1 = Object.keys(dict[group]); - for (var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) { - element = keys1[k1]; - if (dict[group][element][2] === tagName) { - break outLabel; - } - } - } - return {group: group, element: element}; -}; - -/** - * Immutable tag. - * - * @class - * @param {string} group The tag group. - * @param {string} element The tag element. - */ -dwv.dicom.Tag = function (group, element) { - /** - * Get the tag group. - * - * @returns {string} The tag group. - */ - this.getGroup = function () { - return group; - }; - /** - * Get the tag element. - * - * @returns {string} The tag element. - */ - this.getElement = function () { - return element; - }; -}; // Tag class - -/** - * Check for Tag equality. - * - * @param {object} rhs The other tag to compare to. - * @returns {boolean} True if both tags are equal. - */ -dwv.dicom.Tag.prototype.equals = function (rhs) { - return rhs !== null && - this.getGroup() === rhs.getGroup() && - this.getElement() === rhs.getElement(); -}; - -/** - * Check for Tag equality. - * - * @param {object} rhs The other tag to compare to provided as a simple object. - * @returns {boolean} True if both tags are equal. - */ -dwv.dicom.Tag.prototype.equals2 = function (rhs) { - if (rhs === null || - typeof rhs.group === 'undefined' || - typeof rhs.element === 'undefined') { - return false; - } - return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); -}; - -// Get the FileMetaInformationGroupLength Tag. -dwv.dicom.getFileMetaInformationGroupLengthTag = function () { - return new dwv.dicom.Tag('0x0002', '0x0000'); -}; -// Get the Item Tag. -dwv.dicom.getItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE000'); -}; -// Get the ItemDelimitationItem Tag. -dwv.dicom.getItemDelimitationItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE00D'); -}; -// Get the SequenceDelimitationItem Tag. -dwv.dicom.getSequenceDelimitationItemTag = function () { - return new dwv.dicom.Tag('0xFFFE', '0xE0DD'); -}; -// Get the PixelData Tag. -dwv.dicom.getPixelDataTag = function () { - return new dwv.dicom.Tag('0x7FE0', '0x0010'); -}; - -/** - * Get the group-element key used to store DICOM elements. - * - * @param {number} group The DICOM group. - * @param {number} element The DICOM element. - * @returns {string} The key. - */ -dwv.dicom.getGroupElementKey = function (group, element) { - return 'x' + group.substr(2, 6) + element.substr(2, 6); -}; - -/** - * Split a group-element key used to store DICOM elements. - * - * @param {string} key The key in form "x00280102. - * @returns {object} The DICOM group and element. - */ -dwv.dicom.splitGroupElementKey = function (key) { - return {group: key.substr(1, 4), element: key.substr(5, 8)}; -}; - /** * Get patient orientation label in the reverse direction. * @@ -640,7 +161,7 @@ dwv.dicom.isBigEndianTransferSyntax = function (syntax) { */ dwv.dicom.isJpegBaselineTransferSyntax = function (syntax) { return syntax === '1.2.840.10008.1.2.4.50' || - syntax === '1.2.840.10008.1.2.4.51'; + syntax === '1.2.840.10008.1.2.4.51'; }; /** @@ -651,9 +172,9 @@ dwv.dicom.isJpegBaselineTransferSyntax = function (syntax) { */ dwv.dicom.isJpegRetiredTransferSyntax = function (syntax) { return (syntax.match(/1.2.840.10008.1.2.4.5/) !== null && - !dwv.dicom.isJpegBaselineTransferSyntax() && - !dwv.dicom.isJpegLosslessTransferSyntax()) || - syntax.match(/1.2.840.10008.1.2.4.6/) !== null; + !dwv.dicom.isJpegBaselineTransferSyntax() && + !dwv.dicom.isJpegLosslessTransferSyntax()) || + syntax.match(/1.2.840.10008.1.2.4.6/) !== null; }; /** @@ -664,7 +185,7 @@ dwv.dicom.isJpegRetiredTransferSyntax = function (syntax) { */ dwv.dicom.isJpegLosslessTransferSyntax = function (syntax) { return syntax === '1.2.840.10008.1.2.4.57' || - syntax === '1.2.840.10008.1.2.4.70'; + syntax === '1.2.840.10008.1.2.4.70'; }; /** @@ -732,12 +253,12 @@ dwv.dicom.isReadSupportedTransferSyntax = function (syntax) { // dwv.dicom.isJpeglsTransferSyntax(syntax): JPEG-LS return (syntax === '1.2.840.10008.1.2' || // Implicit VR - Little Endian - syntax === '1.2.840.10008.1.2.1' || // Explicit VR - Little Endian - syntax === '1.2.840.10008.1.2.2' || // Explicit VR - Big Endian - dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline - dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless - dwv.dicom.isJpeg2000TransferSyntax(syntax) || // JPEG 2000 - dwv.dicom.isRleTransferSyntax(syntax)); // RLE + syntax === '1.2.840.10008.1.2.1' || // Explicit VR - Little Endian + syntax === '1.2.840.10008.1.2.2' || // Explicit VR - Big Endian + dwv.dicom.isJpegBaselineTransferSyntax(syntax) || // JPEG baseline + dwv.dicom.isJpegLosslessTransferSyntax(syntax) || // JPEG Lossless + dwv.dicom.isJpeg2000TransferSyntax(syntax) || // JPEG 2000 + dwv.dicom.isRleTransferSyntax(syntax)); // RLE }; /** @@ -799,6 +320,72 @@ dwv.dicom.getTransferSyntaxName = function (syntax) { return name; }; +/** + * Guess the transfer syntax from the first data element. + * See https://github.com/ivmartel/dwv/issues/188 + * (Allow to load DICOM with no DICM preamble) for more details. + * + * @param {object} firstDataElement The first data element of the DICOM header. + * @returns {object} The transfer syntax data element. + */ +dwv.dicom.guessTransferSyntax = function (firstDataElement) { + var oEightGroupBigEndian = '0x0800'; + var oEightGroupLittleEndian = '0x0008'; + // check that group is 0x0008 + var group = firstDataElement.tag.group; + if (group !== oEightGroupBigEndian && + group !== oEightGroupLittleEndian) { + throw new Error( + 'Not a valid DICOM file (no magic DICM word found' + + 'and first element not in 0x0008 group)' + ); + } + // reasonable assumption: 2 uppercase characters => explicit vr + var vr = firstDataElement.vr; + var vr0 = vr.charCodeAt(0); + var vr1 = vr.charCodeAt(1); + var implicit = (vr0 >= 65 && vr0 <= 90 && vr1 >= 65 && vr1 <= 90) + ? false : true; + // guess transfer syntax + var syntax = null; + if (group === oEightGroupLittleEndian) { + if (implicit) { + // ImplicitVRLittleEndian + syntax = '1.2.840.10008.1.2'; + } else { + // ExplicitVRLittleEndian + syntax = '1.2.840.10008.1.2.1'; + } + } else { + if (implicit) { + // ImplicitVRBigEndian: impossible + throw new Error( + 'Not a valid DICOM file (no magic DICM word found' + + 'and implicit VR big endian detected)' + ); + } else { + // ExplicitVRBigEndian + syntax = '1.2.840.10008.1.2.2'; + } + } + // set transfer syntax data element + var dataElement = { + tag: { + group: '0x0002', + element: '0x0010', + name: 'x00020010', + endOffset: 4 + }, + vr: 'UI' + }; + dataElement.value = [syntax + ' ']; // even length + dataElement.vl = dataElement.value[0].length; + dataElement.startOffset = firstDataElement.startOffset; + dataElement.endOffset = dataElement.startOffset + dataElement.vl; + + return dataElement; +}; + /** * Get the appropriate TypedArray in function of arguments. * @@ -811,23 +398,31 @@ dwv.dicom.getTransferSyntaxName = function (syntax) { */ dwv.dicom.getTypedArray = function (bitsAllocated, pixelRepresentation, size) { var res = null; - if (bitsAllocated === 8) { - if (pixelRepresentation === 0) { - res = new Uint8Array(size); - } else { - res = new Int8Array(size); - } - } else if (bitsAllocated === 16) { - if (pixelRepresentation === 0) { - res = new Uint16Array(size); - } else { - res = new Int16Array(size); + try { + if (bitsAllocated === 8) { + if (pixelRepresentation === 0) { + res = new Uint8Array(size); + } else { + res = new Int8Array(size); + } + } else if (bitsAllocated === 16) { + if (pixelRepresentation === 0) { + res = new Uint16Array(size); + } else { + res = new Int16Array(size); + } + } else if (bitsAllocated === 32) { + if (pixelRepresentation === 0) { + res = new Uint32Array(size); + } else { + res = new Int32Array(size); + } } - } else if (bitsAllocated === 32) { - if (pixelRepresentation === 0) { - res = new Uint32Array(size); - } else { - res = new Int32Array(size); + } catch (error) { + if (error instanceof RangeError) { + var powerOf2 = Math.floor(Math.log(size) / Math.log(2)); + dwv.logger.error('Cannot allocate array of size: ' + + size + ' (>2^' + powerOf2 + ').'); } } return res; @@ -851,21 +446,6 @@ dwv.dicom.is32bitVLVR = function (vr) { vr === 'UN'); }; -/** - * Does this tag have a VR. - * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. - * - * @param {string} group The tag group. - * @param {string} element The tag element. - * @returns {boolean} True if this tar has a VR. - */ -dwv.dicom.isTagWithVR = function (group, element) { - return !(group === '0xFFFE' && - (element === '0xE000' || element === '0xE00D' || element === '0xE0DD') - ); -}; - - /** * Get the number of bytes occupied by a data element prefix, * i.e. without its value. @@ -953,7 +533,7 @@ dwv.dicom.DicomParser.prototype.getDicomElements = function () { /** * Read a DICOM tag. * - * @param {object} reader The raw data reader. + * @param {dwv.dicom.DataReader} reader The raw data reader. * @param {number} offset The offset where to start to read. * @returns {object} An object containing the tags 'group', * 'element' and 'name'. @@ -966,7 +546,7 @@ dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) { var element = reader.readHex(offset); offset += Uint16Array.BYTES_PER_ELEMENT; // name - var name = dwv.dicom.getGroupElementKey(group, element); + var name = new dwv.dicom.Tag(group, element).getKey(); // return return { group: group, @@ -977,14 +557,47 @@ dwv.dicom.DicomParser.prototype.readTag = function (reader, offset) { }; /** - * Read an item data element. + * Read an explicit item data element. + * + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} offset The offset where to start to read. + * @param {boolean} implicit Is the DICOM VR implicit? + * @returns {object} The item data as a list of data elements. + */ +dwv.dicom.DicomParser.prototype.readExplicitItemDataElement = function ( + reader, offset, implicit) { + var itemData = {}; + + // read the first item + var item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + itemData[item.tag.name] = item; + + // read until the end offset + var endOffset = offset; + offset -= item.vl; + while (offset < endOffset) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + itemData[item.tag.name] = item; + } + + return { + data: itemData, + endOffset: offset, + isSeqDelim: false + }; +}; + +/** + * Read an implicit item data element. * - * @param {object} reader The raw data reader. + * @param {dwv.dicom.DataReader} reader The raw data reader. * @param {number} offset The offset where to start to read. * @param {boolean} implicit Is the DICOM VR implicit? * @returns {object} The item data as a list of data elements. */ -dwv.dicom.DicomParser.prototype.readItemDataElement = function ( +dwv.dicom.DicomParser.prototype.readImplicitItemDataElement = function ( reader, offset, implicit) { var itemData = {}; @@ -993,40 +606,25 @@ dwv.dicom.DicomParser.prototype.readItemDataElement = function ( offset = item.endOffset; // exit if it is a sequence delimitation item - var isSeqDelim = (item.tag.name === 'xFFFEE0DD'); - if (isSeqDelim) { - return {data: itemData, + if (item.tag.name === 'xFFFEE0DD') { + return { + data: itemData, endOffset: item.endOffset, - isSeqDelim: isSeqDelim}; + isSeqDelim: true + }; } - // store it + // store item itemData[item.tag.name] = item; - if (item.vl !== 'u/l') { - // explicit VR items - // not empty - if (item.vl !== 0) { - // read until the end offset - var endOffset = offset; - offset -= item.vl; - while (offset < endOffset) { - item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; - itemData[item.tag.name] = item; - } - } - } else { - // implicit VR items - // read until the item delimitation item - var isItemDelim = false; - while (!isItemDelim) { - item = this.readDataElement(reader, offset, implicit); - offset = item.endOffset; - isItemDelim = (item.tag.name === 'xFFFEE00D'); - if (!isItemDelim) { - itemData[item.tag.name] = item; - } + // read until the item delimitation item + var isItemDelim = false; + while (!isItemDelim) { + item = this.readDataElement(reader, offset, implicit); + offset = item.endOffset; + isItemDelim = item.tag.name === 'xFFFEE00D'; + if (!isItemDelim) { + itemData[item.tag.name] = item; } } @@ -1041,7 +639,7 @@ dwv.dicom.DicomParser.prototype.readItemDataElement = function ( * Read the pixel item data element. * Ref: [Single frame fragments]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_A.4.html#table_A.4-1}. * - * @param {object} reader The raw data reader. + * @param {dwv.dicom.DataReader} reader The raw data reader. * @param {number} offset The offset where to start to read. * @param {boolean} implicit Is the DICOM VR implicit? * @returns {Array} The item data as an array of data elements. @@ -1060,9 +658,9 @@ dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function ( while (!isSeqDelim) { item = this.readDataElement(reader, offset, implicit); offset = item.endOffset; - isSeqDelim = (item.tag.name === 'xFFFEE0DD'); + isSeqDelim = item.tag.name === 'xFFFEE0DD'; if (!isSeqDelim) { - itemData.push(item.value); + itemData.push(item); } } @@ -1077,7 +675,7 @@ dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function ( * Read a DICOM data element. * Reference: [DICOM VRs]{@link http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_6.2.html#table_6.2-1}. * - * @param {object} reader The raw data reader. + * @param {dwv.dicom.DataReader} reader The raw data reader. * @param {number} offset The offset where to start to read. * @param {boolean} implicit Is the DICOM VR implicit? * @returns {object} An object containing the element @@ -1086,20 +684,19 @@ dwv.dicom.DicomParser.prototype.readPixelItemDataElement = function ( dwv.dicom.DicomParser.prototype.readDataElement = function ( reader, offset, implicit) { // Tag: group, element - var tag = this.readTag(reader, offset); - offset = tag.endOffset; + var tagData = this.readTag(reader, offset); + var tag = new dwv.dicom.Tag(tagData.group, tagData.element); + offset = tagData.endOffset; // Value Representation (VR) var vr = null; var is32bitVLVR = false; - if (dwv.dicom.isTagWithVR(tag.group, tag.element)) { + if (tag.isWithVR()) { // implicit VR if (implicit) { - vr = 'UN'; - var dict = dwv.dicom.dictionary; - if (typeof dict[tag.group] !== 'undefined' && - typeof dict[tag.group][tag.element] !== 'undefined') { - vr = dwv.dicom.dictionary[tag.group][tag.element][0]; + vr = tag.getVrFromDictionary(); + if (vr === null) { + vr = 'UN'; } is32bitVLVR = true; } else { @@ -1134,30 +731,103 @@ dwv.dicom.DicomParser.prototype.readDataElement = function ( } // treat private tag with unknown VR and zero VL as a sequence (see #799) - if (dwv.dicom.isPrivateGroup(tag.group) && vr === 'UN' && vl === 0) { + //if (dwv.dicom.isPrivateGroup(tag.group) && vr === 'UN' && vl === 0) { + if (tag.isPrivate() && vr === 'UN' && vl === 0) { vr = 'SQ'; } var startOffset = offset; + var endOffset = startOffset + vl; - // data + // read sequence elements var data = null; - var isPixelData = (tag.name === 'x7FE00010'); - // pixel data sequence (implicit) - if (isPixelData && vlString === 'u/l') { + if (dwv.dicom.isPixelDataTag(tag) && vlString === 'u/l') { + // pixel data sequence (implicit) var pixItemData = this.readPixelItemDataElement(reader, offset, implicit); offset = pixItemData.endOffset; startOffset += pixItemData.offsetTableVl; data = pixItemData.data; - } else if (isPixelData && - (vr === 'OB' || vr === 'OW' || vr === 'OF' || vr === 'ox')) { - // BitsAllocated - var bitsAllocated = 16; - if (typeof this.dicomElements.x00280100 !== 'undefined') { - bitsAllocated = this.dicomElements.x00280100.value[0]; + endOffset = offset; + } else if (vr === 'SQ') { + // sequence + data = []; + var itemData; + if (vlString !== 'u/l') { + // explicit VR sequence + if (vl !== 0) { + // read until the end offset + var sqEndOffset = offset + vl; + while (offset < sqEndOffset) { + itemData = this.readExplicitItemDataElement(reader, offset, implicit); + data.push(itemData.data); + offset = itemData.endOffset; + } + endOffset = offset; + } } else { - dwv.logger.warn('Reading DICOM pixel data with default bitsAllocated.'); + // implicit VR sequence + // read until the sequence delimitation item + var isSeqDelim = false; + while (!isSeqDelim) { + itemData = this.readImplicitItemDataElement(reader, offset, implicit); + isSeqDelim = itemData.isSeqDelim; + offset = itemData.endOffset; + // do not store the delimitation item + if (!isSeqDelim) { + data.push(itemData.data); + } + } + endOffset = offset; } + } + + // return + var element = { + tag: tagData, + vr: vr, + vl: vlString, + startOffset: startOffset, + endOffset: endOffset + }; + if (data) { + element.elements = data; + } + return element; +}; + +/** + * Interpret the data of an element. + * + * @param {object} element The data element. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} pixelRepresentation PixelRepresentation 0->unsigned, + * 1->signed (needed for pixel data or VR=xs). + * @param {number} bitsAllocated Bits allocated (needed for pixel data). + * @returns {object} The interpreted data. + */ +dwv.dicom.DicomParser.prototype.interpretElement = function ( + element, reader, pixelRepresentation, bitsAllocated) { + + var tag = element.tag; + var vl = element.vl; + var vr = element.vr; + var offset = element.startOffset; + + // data + var data = null; + var isPixelDataTag = dwv.dicom.isPixelDataTag( + new dwv.dicom.Tag(tag.group, tag.element)); + if (isPixelDataTag && vl === 'u/l') { + // implicit pixel data sequence + data = []; + for (var j = 0; j < element.elements.length; ++j) { + data.push(this.interpretElement( + element.elements[j], reader, + pixelRepresentation, bitsAllocated)); + } + } else if (isPixelDataTag && + (vr === 'OB' || vr === 'OW' || vr === 'OF' || vr === 'ox')) { + // check bits allocated and VR if (bitsAllocated === 8 && vr === 'OW') { dwv.logger.warn( 'Reading DICOM pixel data with vr=OW' + @@ -1170,96 +840,66 @@ dwv.dicom.DicomParser.prototype.readDataElement = function ( ' and bitsAllocated=16 (should be 8).' ); } - // PixelRepresentation 0->unsigned, 1->signed - var pixelRepresentation = 0; - if (typeof this.dicomElements.x00280103 !== 'undefined') { - pixelRepresentation = this.dicomElements.x00280103.value[0]; - } else { - dwv.logger.warn( - 'Reading DICOM pixel data with default pixelRepresentation.' - ); - } // read + data = []; if (bitsAllocated === 1) { - data = reader.readBinaryArray(offset, vl); + data.push(reader.readBinaryArray(offset, vl)); } else if (bitsAllocated === 8) { if (pixelRepresentation === 0) { - data = reader.readUint8Array(offset, vl); + data.push(reader.readUint8Array(offset, vl)); } else { - data = reader.readInt8Array(offset, vl); + data.push(reader.readInt8Array(offset, vl)); } } else if (bitsAllocated === 16) { if (pixelRepresentation === 0) { - data = reader.readUint16Array(offset, vl); + data.push(reader.readUint16Array(offset, vl)); } else { - data = reader.readInt16Array(offset, vl); + data.push(reader.readInt16Array(offset, vl)); } } else if (bitsAllocated === 32) { if (pixelRepresentation === 0) { - data = reader.readUint32Array(offset, vl); + data.push(reader.readUint32Array(offset, vl)); } else { - data = reader.readInt32Array(offset, vl); + data.push(reader.readInt32Array(offset, vl)); } } else if (bitsAllocated === 64) { if (pixelRepresentation === 0) { - data = reader.readUint64Array(offset, vl); + data.push(reader.readUint64Array(offset, vl)); } else { - data = reader.readInt64Array(offset, vl); + data.push(reader.readInt64Array(offset, vl)); } } else { throw new Error('Unsupported bits allocated: ' + bitsAllocated); } - offset += vl; } else if (vr === 'OB') { data = reader.readUint8Array(offset, vl); - offset += vl; } else if (vr === 'OW') { data = reader.readUint16Array(offset, vl); - offset += vl; } else if (vr === 'OF') { data = reader.readUint32Array(offset, vl); - offset += vl; } else if (vr === 'OD') { data = reader.readUint64Array(offset, vl); - offset += vl; } else if (vr === 'US') { data = reader.readUint16Array(offset, vl); - offset += vl; } else if (vr === 'UL') { data = reader.readUint32Array(offset, vl); - offset += vl; } else if (vr === 'SS') { data = reader.readInt16Array(offset, vl); - offset += vl; } else if (vr === 'SL') { data = reader.readInt32Array(offset, vl); - offset += vl; } else if (vr === 'FL') { data = reader.readFloat32Array(offset, vl); - offset += vl; } else if (vr === 'FD') { data = reader.readFloat64Array(offset, vl); - offset += vl; } else if (vr === 'xs') { - // PixelRepresentation 0->unsigned, 1->signed - var pixelRep = 0; - if (typeof this.dicomElements.x00280103 !== 'undefined') { - pixelRep = this.dicomElements.x00280103.value[0]; - } else { - dwv.logger.warn( - 'Reading DICOM pixel data with default pixelRepresentation.'); - } - // read - if (pixelRep === 0) { + if (pixelRepresentation === 0) { data = reader.readUint16Array(offset, vl); } else { data = reader.readInt16Array(offset, vl); } - offset += vl; } else if (vr === 'AT') { // attribute var raw = reader.readUint16Array(offset, vl); - offset += vl; data = []; for (var i = 0, leni = raw.length; i < leni; i += 2) { var stri = raw[i].toString(16); @@ -1274,57 +914,57 @@ dwv.dicom.DicomParser.prototype.readDataElement = function ( } else if (vr === 'UN') { // not available data = reader.readUint8Array(offset, vl); - offset += vl; } else if (vr === 'SQ') { // sequence data = []; - var itemData; - // explicit VR sequence - if (vlString !== 'u/l') { - // not empty - if (vl !== 0) { - var sqEndOffset = offset + vl; - while (offset < sqEndOffset) { - itemData = this.readItemDataElement(reader, offset, implicit); - data.push(itemData.data); - offset = itemData.endOffset; - } - } - } else { - // implicit VR sequence - // read until the sequence delimitation item - var isSeqDelim = false; - while (!isSeqDelim) { - itemData = this.readItemDataElement(reader, offset, implicit); - isSeqDelim = itemData.isSeqDelim; - offset = itemData.endOffset; - // do not store the delimitation item - if (!isSeqDelim) { - data.push(itemData.data); - } + for (var k = 0; k < element.elements.length; ++k) { + var item = element.elements[k]; + var itemData = {}; + var keys = Object.keys(item); + for (var l = 0; l < keys.length; ++l) { + var subElement = item[keys[l]]; + subElement.value = this.interpretElement( + subElement, reader, + pixelRepresentation, bitsAllocated); + itemData[keys[l]] = subElement; } + data.push(itemData); } } else { // raw if (vr === 'SH' || vr === 'LO' || vr === 'ST' || - vr === 'PN' || vr === 'LT' || vr === 'UT') { + vr === 'PN' || vr === 'LT' || vr === 'UT') { data = reader.readSpecialString(offset, vl); } else { data = reader.readString(offset, vl); } - offset += vl; data = data.split('\\'); } - // return - return { - tag: tag, - vr: vr, - vl: vlString, - value: data, - startOffset: startOffset, - endOffset: offset - }; + return data; +}; + +/** + * Interpret the data of a list of elements. + * + * @param {Array} elements A list of data elements. + * @param {dwv.dicom.DataReader} reader The raw data reader. + * @param {number} pixelRepresentation PixelRepresentation 0->unsigned, + * 1->signed. + * @param {number} bitsAllocated Bits allocated. + */ +dwv.dicom.DicomParser.prototype.interpret = function ( + elements, reader, + pixelRepresentation, bitsAllocated) { + + var keys = Object.keys(elements); + for (var i = 0; i < keys.length; ++i) { + var element = elements[keys[i]]; + if (typeof element.value === 'undefined') { + element.value = this.interpretElement( + element, reader, pixelRepresentation, bitsAllocated); + } + } }; /** @@ -1335,7 +975,6 @@ dwv.dicom.DicomParser.prototype.readDataElement = function ( */ dwv.dicom.DicomParser.prototype.parse = function (buffer) { var offset = 0; - var implicit = false; var syntax = ''; var dataElement = null; // default readers @@ -1349,6 +988,8 @@ dwv.dicom.DicomParser.prototype.parse = function (buffer) { if (magicword === 'DICM') { // 0x0002, 0x0000: FileMetaInformationGroupLength dataElement = this.readDataElement(metaReader, offset, false); + dataElement.value = this.interpretElement(dataElement, metaReader); + // increment offset offset = dataElement.endOffset; // store the data element this.dicomElements[dataElement.tag.name] = dataElement; @@ -1364,77 +1005,35 @@ dwv.dicom.DicomParser.prototype.parse = function (buffer) { // store the data element this.dicomElements[dataElement.tag.name] = dataElement; } + + // check the TransferSyntaxUID (has to be there!) + dataElement = this.dicomElements.x00020010; + if (typeof dataElement === 'undefined') { + throw new Error('Not a valid DICOM file (no TransferSyntaxUID found)'); + } + dataElement.value = this.interpretElement(dataElement, metaReader); + syntax = dwv.dicom.cleanString(dataElement.value[0]); + } else { - // no metadata: attempt to detect transfer syntax - // see https://github.com/ivmartel/dwv/issues/188 - // (Allow to load DICOM with no DICM preamble) for more details - var oEightGroupBigEndian = '0x0800'; - var oEightGroupLittleEndian = '0x0008'; // read first element - dataElement = this.readDataElement(dataReader, 0, implicit); - // check that group is 0x0008 - if ((dataElement.tag.group !== oEightGroupBigEndian) && - (dataElement.tag.group !== oEightGroupLittleEndian)) { - throw new Error( - 'Not a valid DICOM file (no magic DICM word found' + - 'and first element not in 0x0008 group)' - ); - } - // reasonable assumption: 2 uppercase characters => explicit vr - var vr0 = dataElement.vr.charCodeAt(0); - var vr1 = dataElement.vr.charCodeAt(1); - implicit = (vr0 >= 65 && vr0 <= 90 && vr1 >= 65 && vr1 <= 90) - ? false : true; + dataElement = this.readDataElement(dataReader, 0, false); // guess transfer syntax - if (dataElement.tag.group === oEightGroupLittleEndian) { - if (implicit) { - // ImplicitVRLittleEndian - syntax = '1.2.840.10008.1.2'; - } else { - // ExplicitVRLittleEndian - syntax = '1.2.840.10008.1.2.1'; - } - } else { - if (implicit) { - // ImplicitVRBigEndian: impossible - throw new Error( - 'Not a valid DICOM file (no magic DICM word found' + - 'and implicit VR big endian detected)' - ); - } else { - // ExplicitVRBigEndian - syntax = '1.2.840.10008.1.2.2'; - } - } - // set transfer syntax data element - dataElement.tag.group = '0x0002'; - dataElement.tag.element = '0x0010'; - dataElement.tag.name = 'x00020010'; - dataElement.tag.endOffset = 4; - dataElement.vr = 'UI'; - dataElement.value = [syntax + ' ']; // even length - dataElement.vl = dataElement.value[0].length; - dataElement.endOffset = dataElement.startOffset + dataElement.vl; - // store it - this.dicomElements[dataElement.tag.name] = dataElement; - + var tsElement = dwv.dicom.guessTransferSyntax(dataElement); + // store + this.dicomElements[tsElement.tag.name] = tsElement; + syntax = dwv.dicom.cleanString(tsElement.value[0]); // reset offset offset = 0; } - // check the TransferSyntaxUID (has to be there!) - if (typeof this.dicomElements.x00020010 === 'undefined') { - throw new Error('Not a valid DICOM file (no TransferSyntaxUID found)'); - } - syntax = dwv.dicom.cleanString(this.dicomElements.x00020010.value[0]); - - // check support + // check transfer syntax support if (!dwv.dicom.isReadSupportedTransferSyntax(syntax)) { throw new Error('Unsupported DICOM transfer syntax: \'' + syntax + - '\' (' + dwv.dicom.getTransferSyntaxName(syntax) + ')'); + '\' (' + dwv.dicom.getTransferSyntaxName(syntax) + ')'); } - // Implicit VR + // set implicit flag + var implicit = false; if (dwv.dicom.isImplicitTransferSyntax(syntax)) { implicit = true; } @@ -1453,18 +1052,6 @@ dwv.dicom.DicomParser.prototype.parse = function (buffer) { while (offset < buffer.byteLength) { // get the data element dataElement = this.readDataElement(dataReader, offset, implicit); - // check character set - if (dataElement.tag.name === 'x00080005') { - var charSetTerm; - if (dataElement.value.length === 1) { - charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); - } else { - charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); - dwv.logger.warn('Unsupported character set with code extensions: \'' + - charSetTerm + '\'.'); - } - dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); - } // increment offset offset = dataElement.endOffset; // store the data element @@ -1475,65 +1062,73 @@ dwv.dicom.DicomParser.prototype.parse = function (buffer) { } } - // safety check... + // safety checks... + if (isNaN(offset)) { + throw new Error('Problem while parsing, bad offset'); + } if (buffer.byteLength !== offset) { dwv.logger.warn('Did not reach the end of the buffer: ' + offset + ' != ' + buffer.byteLength); } - // pixel buffer - if (typeof this.dicomElements.x7FE00010 !== 'undefined') { + //------------------------------------------------- + // values needed for data interpretation + + // PixelRepresentation 0->unsigned, 1->signed + var pixelRepresentation = 0; + dataElement = this.dicomElements.x00280103; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + pixelRepresentation = dataElement.value[0]; + } else { + dwv.logger.warn( + 'Reading DICOM pixel data with default pixelRepresentation.'); + } + + // BitsAllocated + var bitsAllocated = 16; + dataElement = this.dicomElements.x00280100; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + bitsAllocated = dataElement.value[0]; + } else { + dwv.logger.warn('Reading DICOM pixel data with default bitsAllocated.'); + } - var numberOfFrames = 1; - if (typeof this.dicomElements.x00280008 !== 'undefined') { - numberOfFrames = dwv.dicom.cleanString( - this.dicomElements.x00280008.value[0]); + // character set + dataElement = this.dicomElements.x00080005; + if (typeof dataElement !== 'undefined') { + dataElement.value = this.interpretElement(dataElement, dataReader); + var charSetTerm; + if (dataElement.value.length === 1) { + charSetTerm = dwv.dicom.cleanString(dataElement.value[0]); + } else { + charSetTerm = dwv.dicom.cleanString(dataElement.value[1]); + dwv.logger.warn('Unsupported character set with code extensions: \'' + + charSetTerm + '\'.'); } + dataReader.setUtfLabel(dwv.dicom.getUtfLabel(charSetTerm)); + } - if (this.dicomElements.x7FE00010.vl !== 'u/l') { - // compressed should be encapsulated... - if (dwv.dicom.isJpeg2000TransferSyntax(syntax) || - dwv.dicom.isJpegBaselineTransferSyntax(syntax) || - dwv.dicom.isJpegLosslessTransferSyntax(syntax)) { - dwv.logger.warn('Compressed but no items...'); - } + // interpret the dicom elements + this.interpret( + this.dicomElements, dataReader, + pixelRepresentation, bitsAllocated + ); - // calculate the slice size - var pixData = this.dicomElements.x7FE00010.value; - if (pixData && typeof pixData !== 'undefined' && - pixData.length !== 0) { - if (typeof this.dicomElements.x00280010 === 'undefined') { - throw new Error('Missing image number of rows.'); - } - if (typeof this.dicomElements.x00280011 === 'undefined') { - throw new Error('Missing image number of columns.'); - } - if (typeof this.dicomElements.x00280002 === 'undefined') { - throw new Error('Missing image samples per pixel.'); - } - var columns = this.dicomElements.x00280011.value[0]; - var rows = this.dicomElements.x00280010.value[0]; - var samplesPerPixel = this.dicomElements.x00280002.value[0]; - var sliceSize = columns * rows * samplesPerPixel; - // slice data in an array of frames - var newPixData = []; - var frameOffset = 0; - for (var g = 0; g < numberOfFrames; ++g) { - newPixData[g] = pixData.slice(frameOffset, frameOffset + sliceSize); - frameOffset += sliceSize; - } - // store as pixel data - this.dicomElements.x7FE00010.value = newPixData; - } else { - dwv.logger.info('Empty pixel data.'); + // handle fragmented pixel buffer + // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html + // (third note, "Depending on the transfer syntax...") + dataElement = this.dicomElements.x7FE00010; + if (typeof dataElement !== 'undefined') { + if (dataElement.vl === 'u/l') { + var numberOfFrames = 1; + if (typeof this.dicomElements.x00280008 !== 'undefined') { + numberOfFrames = dwv.dicom.cleanString( + this.dicomElements.x00280008.value[0]); } - } else { - // handle fragmented pixel buffer - // Reference: http://dicom.nema.org/dicom/2013/output/chtml/part05/sect_8.2.html - // (third note, "Depending on the transfer syntax...") - var pixItems = this.dicomElements.x7FE00010.value; + var pixItems = dataElement.value; if (pixItems.length > 1 && pixItems.length > numberOfFrames) { - // concatenate pixel data items // concat does not work on typed arrays //this.pixelBuffer = this.pixelBuffer.concat( dataElement.data ); @@ -1559,7 +1154,7 @@ dwv.dicom.DicomParser.prototype.parse = function (buffer) { newPixItems[f] = newBuffer; } // store as pixel data - this.dicomElements.x7FE00010.value = newPixItems; + dataElement.value = newPixItems; } } } diff --git a/src/dicom/dicomTag.js b/src/dicom/dicomTag.js new file mode 100644 index 0000000000..09053cc4a6 --- /dev/null +++ b/src/dicom/dicomTag.js @@ -0,0 +1,298 @@ +// namespaces +var dwv = dwv || {}; +dwv.dicom = dwv.dicom || {}; + +/** + * Immutable tag. + * + * @class + * @param {string} group The tag group as '0x####'. + * @param {string} element The tag element as '0x####'. + */ +dwv.dicom.Tag = function (group, element) { + /** + * Get the tag group. + * + * @returns {string} The tag group. + */ + this.getGroup = function () { + return group; + }; + /** + * Get the tag element. + * + * @returns {string} The tag element. + */ + this.getElement = function () { + return element; + }; +}; // Tag class + +/** + * Check for Tag equality. + * + * @param {dwv.dicom.Tag} rhs The other tag to compare to. + * @returns {boolean} True if both tags are equal. + */ +dwv.dicom.Tag.prototype.equals = function (rhs) { + return rhs !== null && + this.getGroup() === rhs.getGroup() && + this.getElement() === rhs.getElement(); +}; + +/** + * Check for Tag equality. + * + * @param {object} rhs The other tag to compare to provided as a simple object. + * @returns {boolean} True if both tags are equal. + */ +dwv.dicom.Tag.prototype.equals2 = function (rhs) { + if (rhs === null || + typeof rhs.group === 'undefined' || + typeof rhs.element === 'undefined') { + return false; + } + return this.equals(new dwv.dicom.Tag(rhs.group, rhs.element)); +}; + +/** + * Get the group-element key used to store DICOM elements. + * + * @returns {string} The key. + */ +dwv.dicom.Tag.prototype.getKey = function () { + return 'x' + this.getGroup().substr(2, 6) + this.getElement().substr(2, 6); +}; + +/** + * Get a simplified group-element key. + * + * @returns {string} The key. + */ +dwv.dicom.Tag.prototype.getKey2 = function () { + return this.getGroup().substr(2, 6) + this.getElement().substr(2, 6); +}; + +/** + * Get the group name as defined in dwv.dicom.TagGroups. + * + * @returns {string} The name. + */ +dwv.dicom.Tag.prototype.getGroupName = function () { + return dwv.dicom.TagGroups[this.getGroup().substr(1)]; +}; + + +/** + * Split a group-element key used to store DICOM elements. + * + * @param {string} key The key in form "x00280102" as generated by tag::getKey. + * @returns {object} The DICOM tag. + */ +dwv.dicom.getTagFromKey = function (key) { + return new dwv.dicom.Tag(key.substr(1, 4), key.substr(5, 8)); +}; + +/** + * Does this tag have a VR. + * Basically the Item, ItemDelimitationItem and SequenceDelimitationItem tags. + * + * @returns {boolean} True if this tag has a VR. + */ +dwv.dicom.Tag.prototype.isWithVR = function () { + var element = this.getElement(); + return !(this.getGroup() === '0xFFFE' && + (element === '0xE000' || element === '0xE00D' || element === '0xE0DD') + ); +}; + +/** + * Is the tag group a private tag group ? + * see: http://dicom.nema.org/medical/dicom/2015a/output/html/part05.html#sect_7.8 + * + * @returns {boolean} True if the tag group is private, + * ie if its group is an odd number. + */ +dwv.dicom.Tag.prototype.isPrivate = function () { + var groupNumber = parseInt(this.getGroup().substr(2, 6), 10); + return groupNumber % 2 === 1; +}; + +/** + * Get the tag info from the dicom dictionary. + * + * @returns {Array} The info as [vr, multiplicity, name]. + */ +dwv.dicom.Tag.prototype.getInfoFromDictionary = function () { + var info = null; + if (typeof dwv.dicom.dictionary[this.getGroup()] !== 'undefined' && + typeof dwv.dicom.dictionary[this.getGroup()][this.getElement()] !== + 'undefined') { + info = dwv.dicom.dictionary[this.getGroup()][this.getElement()]; + } + return info; +}; + +/** + * Get the tag Value Representation (VR) from the dicom dictionary. + * + * @returns {string} The VR. + */ +dwv.dicom.Tag.prototype.getVrFromDictionary = function () { + var vr = null; + var info = this.getInfoFromDictionary(); + if (info !== null) { + vr = info[0]; + } + return vr; +}; + +/** + * Get the tag name from the dicom dictionary. + * + * @returns {string} The VR. + */ +dwv.dicom.Tag.prototype.getNameFromDictionary = function () { + var name = null; + var info = this.getInfoFromDictionary(); + if (info !== null) { + name = info[2]; + } + return name; +}; + +/** + * Get the TransferSyntaxUID Tag. + * + * @returns {object} The tag. + */ +dwv.dicom.getTransferSyntaxUIDTag = function () { + return new dwv.dicom.Tag('0x0002', '0x0010'); +}; + +/** + * Get the FileMetaInformationGroupLength Tag. + * + * @returns {object} The tag. + */ +dwv.dicom.getFileMetaInformationGroupLengthTag = function () { + return new dwv.dicom.Tag('0x0002', '0x0000'); +}; + +/** + * Is the input tag the FileMetaInformationGroupLength Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isFileMetaInformationGroupLengthTag = function (tag) { + return tag.equals(dwv.dicom.getFileMetaInformationGroupLengthTag()); +}; + +/** + * Get the Item Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE000'); +}; + +/** + * Is the input tag the Item Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isItemTag = function (tag) { + return tag.equals(dwv.dicom.getItemTag()); +}; + +/** + * Get the ItemDelimitationItem Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getItemDelimitationItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE00D'); +}; + +/** + * Is the input tag the ItemDelimitationItem Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isItemDelimitationItemTag = function (tag) { + return tag.equals(dwv.dicom.getItemDelimitationItemTag()); +}; + +/** + * Get the SequenceDelimitationItem Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getSequenceDelimitationItemTag = function () { + return new dwv.dicom.Tag('0xFFFE', '0xE0DD'); +}; + +/** + * Is the input tag the SequenceDelimitationItem Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isSequenceDelimitationItemTag = function (tag) { + return tag.equals(dwv.dicom.getSequenceDelimitationItemTag()); +}; + +/** + * Get the PixelData Tag. + * + * @returns {dwv.dicom.Tag} The tag. + */ +dwv.dicom.getPixelDataTag = function () { + return new dwv.dicom.Tag('0x7FE0', '0x0010'); +}; + +/** + * Is the input tag the PixelData Tag. + * + * @param {dwv.dicom.Tag} tag The tag to test. + * @returns {boolean} True if the asked tag. + */ +dwv.dicom.isPixelDataTag = function (tag) { + return tag.equals(dwv.dicom.getPixelDataTag()); +}; + +/** + * Get a tag from the dictionary using a tag string name. + * + * @param {string} tagName The tag string name. + * @returns {object} The tag object. + */ +dwv.dicom.getTagFromDictionary = function (tagName) { + var group = null; + var element = null; + var dict = dwv.dicom.dictionary; + var keys0 = Object.keys(dict); + var keys1 = null; + // label for nested loop break + outLabel: + // search through dictionary + for (var k0 = 0, lenK0 = keys0.length; k0 < lenK0; ++k0) { + group = keys0[k0]; + keys1 = Object.keys(dict[group]); + for (var k1 = 0, lenK1 = keys1.length; k1 < lenK1; ++k1) { + element = keys1[k1]; + if (dict[group][element][2] === tagName) { + break outLabel; + } + } + } + var tag = null; + if (group !== null && element !== null) { + tag = new dwv.dicom.Tag(group, element); + } + return tag; +}; diff --git a/src/dicom/dicomWriter.js b/src/dicom/dicomWriter.js index 5dd4e9cce8..4bbe689bd5 100644 --- a/src/dicom/dicomWriter.js +++ b/src/dicom/dicomWriter.js @@ -121,7 +121,68 @@ dwv.dicom.padElementValue = function (element, value) { }; /** - * Data writer. + * Is this element an implicit length sequence? + * + * @param {object} element The element to check. + * @returns {boolean} True if it is. + */ +dwv.dicom.isImplicitLengthSequence = function (element) { + // sequence with no length + return (element.vr === 'SQ') && + (element.vl === 'u/l'); +}; + +/** + * Is this element an implicit length item? + * + * @param {object} element The element to check. + * @returns {boolean} True if it is. + */ +dwv.dicom.isImplicitLengthItem = function (element) { + // item with no length + return (element.tag.name === 'xFFFEE000') && + (element.vl === 'u/l'); +}; + +/** + * Is this element an implicit length pixel data? + * + * @param {object} element The element to check. + * @returns {boolean} True if it is. + */ +dwv.dicom.isImplicitLengthPixels = function (element) { + // pixel data with no length + return (element.tag.name === 'x7FE00010') && + (element.vl === 'u/l'); +}; + +/** + * Helper method to flatten an array of typed arrays to 2D typed array + * + * @param {Array} initialArray array of typed arrays + * @returns {object} a typed array containing all values + */ +dwv.dicom.flattenArrayOfTypedArrays = function (initialArray) { + var initialArrayLength = initialArray.length; + var arrayLength = initialArray[0].length; + // If this is not a array of arrays, just return the initial one: + if (typeof arrayLength === 'undefined') { + return initialArray; + } + + var flattenendArrayLength = initialArrayLength * arrayLength; + + var flattenedArray = new initialArray[0].constructor(flattenendArrayLength); + + for (var i = 0; i < initialArrayLength; i++) { + var indexFlattenedArray = i * arrayLength; + flattenedArray.set(initialArray[i], indexFlattenedArray); + } + return flattenedArray; +}; + +/** + * DICOM writer. * * Example usage: * var parser = new dwv.dicom.DicomParser(); @@ -135,317 +196,109 @@ dwv.dicom.padElementValue = function (element, value) { * element.download = "anonym.dcm"; * * @class - * @param {Array} buffer The input array buffer. - * @param {boolean} isLittleEndian Flag to tell if the data is - * little or big endian. */ -dwv.dicom.DataWriter = function (buffer, isLittleEndian) { - // Set endian flag if not defined. - if (typeof isLittleEndian === 'undefined') { - isLittleEndian = true; - } - - // private DataView - var view = new DataView(buffer); +dwv.dicom.DicomWriter = function () { // flag to use VR=UN for private sequences, default to false // (mainly used in tests) this.useUnVrForPrivateSq = false; - /** - * Write Uint8 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint8 = function (byteOffset, value) { - view.setUint8(byteOffset, value); - return byteOffset + Uint8Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Int8 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt8 = function (byteOffset, value) { - view.setInt8(byteOffset, value); - return byteOffset + Int8Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Uint16 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint16 = function (byteOffset, value) { - view.setUint16(byteOffset, value, isLittleEndian); - return byteOffset + Uint16Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Int16 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt16 = function (byteOffset, value) { - view.setInt16(byteOffset, value, isLittleEndian); - return byteOffset + Int16Array.BYTES_PER_ELEMENT; - }; - - /** - * Write Uint32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeUint32 = function (byteOffset, value) { - view.setUint32(byteOffset, value, isLittleEndian); - return byteOffset + Uint32Array.BYTES_PER_ELEMENT; + // possible tag actions + var actions = { + copy: function (item) { + return item; + }, + remove: function () { + return null; + }, + clear: function (item) { + item.value[0] = ''; + item.vl = 0; + item.endOffset = item.startOffset; + return item; + }, + replace: function (item, value) { + var paddedValue = dwv.dicom.padElementValue(item, value); + item.value[0] = paddedValue; + item.vl = paddedValue.length; + item.endOffset = item.startOffset + paddedValue.length; + return item; + } }; - /** - * Write Int32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. - */ - this.writeInt32 = function (byteOffset, value) { - view.setInt32(byteOffset, value, isLittleEndian); - return byteOffset + Int32Array.BYTES_PER_ELEMENT; + // default rules: just copy + var defaultRules = { + default: {action: 'copy', value: null} }; /** - * Write Float32 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. + * Public (modifiable) rules. + * Set of objects as: + * name : { action: 'actionName', value: 'optionalValue } + * The names are either 'default', tagName or groupName. + * Each DICOM element will be checked to see if a rule is applicable. + * First checked by tagName and then by groupName, + * if nothing is found the default rule is applied. */ - this.writeFloat32 = function (byteOffset, value) { - view.setFloat32(byteOffset, value, isLittleEndian); - return byteOffset + Float32Array.BYTES_PER_ELEMENT; - }; + this.rules = defaultRules; /** - * Write Float64 data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} value The data to write. - * @returns {number} The new offset position. + * Example anonymisation rules. */ - this.writeFloat64 = function (byteOffset, value) { - view.setFloat64(byteOffset, value, isLittleEndian); - return byteOffset + Float64Array.BYTES_PER_ELEMENT; + this.anonymisationRules = { + default: {action: 'remove', value: null}, + PatientName: {action: 'replace', value: 'Anonymized'}, // tag + 'Meta Element': {action: 'copy', value: null}, // group 'x0002' + Acquisition: {action: 'copy', value: null}, // group 'x0018' + 'Image Presentation': {action: 'copy', value: null}, // group 'x0028' + Procedure: {action: 'copy', value: null}, // group 'x0040' + 'Pixel Data': {action: 'copy', value: null} // group 'x7fe0' }; /** - * Write string data as hexadecimal. + * Get the element to write according to the class rules. + * Priority order: tagName, groupName, default. * - * @param {number} byteOffset The offset to start writing from. - * @param {number} str The padded hexadecimal string to write ('0x####'). - * @returns {number} The new offset position. + * @param {object} element The element to check + * @returns {object} The element to write, can be null. */ - this.writeHex = function (byteOffset, str) { - // remove first two chars and parse - var value = parseInt(str.substr(2), 16); - view.setUint16(byteOffset, value, isLittleEndian); - return byteOffset + Uint16Array.BYTES_PER_ELEMENT; - }; + this.getElementToWrite = function (element) { + // get group and tag string name + var tag = new dwv.dicom.Tag(element.tag.group, element.tag.element); + var groupName = tag.getGroupName(); + var tagName = tag.getNameFromDictionary(); - /** - * Write string data. - * - * @param {number} byteOffset The offset to start writing from. - * @param {number} str The data to write. - * @returns {number} The new offset position. - */ - this.writeString = function (byteOffset, str) { - for (var i = 0, len = str.length; i < len; ++i) { - view.setUint8(byteOffset, str.charCodeAt(i)); - byteOffset += Uint8Array.BYTES_PER_ELEMENT; + // apply rules: + var rule; + if (typeof this.rules[element.tag.name] !== 'undefined') { + // 1. tag itself + rule = this.rules[element.tag.name]; + } else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') { + // 2. tag name + rule = this.rules[tagName]; + } else if (typeof this.rules[groupName] !== 'undefined') { + // 3. group name + rule = this.rules[groupName]; + } else { + // 4. default + rule = this.rules['default']; } - return byteOffset; + // apply action on element and return + return actions[rule.action](element, rule.value); }; - -}; - -/** - * Write a boolean array as binary. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeBinaryArray = function (byteOffset, array) { - if (array.length % 8 !== 0) { - throw new Error('Cannot write boolean array as binary.'); - } - var byte = null; - var val = null; - for (var i = 0, len = array.length; i < len; i += 8) { - byte = 0; - for (var j = 0; j < 8; ++j) { - val = array[i + j] === 0 ? 0 : 1; - byte += val << j; - } - byteOffset = this.writeUint8(byteOffset, byte); - } - return byteOffset; -}; - -/** - * Write Uint8 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeUint8Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint8(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Int8 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeInt8Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt8(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Uint16 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeUint16Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint16(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Int16 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeInt16Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt16(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Uint32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeUint32Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeUint32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Int32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeInt32Array = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeInt32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Float32 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeFloat32Array = function ( - byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeFloat32(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write Float64 array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeFloat64Array = function ( - byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - byteOffset = this.writeFloat64(byteOffset, array[i]); - } - return byteOffset; -}; - -/** - * Write string array. - * - * @param {number} byteOffset The offset to start writing from. - * @param {Array} array The array to write. - * @returns {number} The new offset position. - */ -dwv.dicom.DataWriter.prototype.writeStringArray = function (byteOffset, array) { - for (var i = 0, len = array.length; i < len; ++i) { - // separator - if (i !== 0) { - byteOffset = this.writeString(byteOffset, '\\'); - } - // value - byteOffset = this.writeString(byteOffset, array[i].toString()); - } - return byteOffset; }; /** * Write a list of items. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {number} byteOffset The offset to start writing from. * @param {Array} items The list of items to write. * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( - byteOffset, items, isImplicit) { +dwv.dicom.DicomWriter.prototype.writeDataElementItems = function ( + writer, byteOffset, items, isImplicit) { var item = null; for (var i = 0; i < items.length; ++i) { item = items[i]; @@ -461,12 +314,13 @@ dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( vl: implicitLength ? 0xffffffff : item.xFFFEE000.vl, value: [] }; - byteOffset = this.writeDataElement(itemElement, byteOffset, isImplicit); + byteOffset = this.writeDataElement( + writer, itemElement, byteOffset, isImplicit); // write rest for (var m = 0; m < itemKeys.length; ++m) { if (itemKeys[m] !== 'xFFFEE000' && itemKeys[m] !== 'xFFFEE00D') { byteOffset = this.writeDataElement( - item[itemKeys[m]], byteOffset, isImplicit); + writer, item[itemKeys[m]], byteOffset, isImplicit); } } // item delimitation @@ -482,7 +336,7 @@ dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( value: [] }; byteOffset = this.writeDataElement( - itemDelimElement, byteOffset, isImplicit); + writer, itemDelimElement, byteOffset, isImplicit); } } @@ -493,6 +347,7 @@ dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( /** * Write data with a specific Value Representation (VR). * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {string} vr The data Value Representation (VR). * @param {string} vl The data Value Length (VL). * @param {number} byteOffset The offset to start writing from. @@ -500,52 +355,53 @@ dwv.dicom.DataWriter.prototype.writeDataElementItems = function ( * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeDataElementValue = function ( - vr, vl, byteOffset, value, isImplicit) { +dwv.dicom.DicomWriter.prototype.writeDataElementValue = function ( + writer, vr, vl, byteOffset, value, isImplicit) { // first check input type to know how to write if (value instanceof Uint8Array) { // binary data has been expanded 8 times at read if (value.length === 8 * vl) { - byteOffset = this.writeBinaryArray(byteOffset, value); + byteOffset = writer.writeBinaryArray(byteOffset, value); } else { - byteOffset = this.writeUint8Array(byteOffset, value); + byteOffset = writer.writeUint8Array(byteOffset, value); } } else if (value instanceof Int8Array) { - byteOffset = this.writeInt8Array(byteOffset, value); + byteOffset = writer.writeInt8Array(byteOffset, value); } else if (value instanceof Uint16Array) { - byteOffset = this.writeUint16Array(byteOffset, value); + byteOffset = writer.writeUint16Array(byteOffset, value); } else if (value instanceof Int16Array) { - byteOffset = this.writeInt16Array(byteOffset, value); + byteOffset = writer.writeInt16Array(byteOffset, value); } else if (value instanceof Uint32Array) { - byteOffset = this.writeUint32Array(byteOffset, value); + byteOffset = writer.writeUint32Array(byteOffset, value); } else if (value instanceof Int32Array) { - byteOffset = this.writeInt32Array(byteOffset, value); + byteOffset = writer.writeInt32Array(byteOffset, value); } else { // switch according to VR if input type is undefined if (vr === 'UN') { - byteOffset = this.writeUint8Array(byteOffset, value); + byteOffset = writer.writeUint8Array(byteOffset, value); } else if (vr === 'OB') { - byteOffset = this.writeInt8Array(byteOffset, value); + byteOffset = writer.writeInt8Array(byteOffset, value); } else if (vr === 'OW') { - byteOffset = this.writeInt16Array(byteOffset, value); + byteOffset = writer.writeInt16Array(byteOffset, value); } else if (vr === 'OF') { - byteOffset = this.writeInt32Array(byteOffset, value); + byteOffset = writer.writeInt32Array(byteOffset, value); } else if (vr === 'OD') { - byteOffset = this.writeInt64Array(byteOffset, value); + byteOffset = writer.writeInt64Array(byteOffset, value); } else if (vr === 'US') { - byteOffset = this.writeUint16Array(byteOffset, value); + byteOffset = writer.writeUint16Array(byteOffset, value); } else if (vr === 'SS') { - byteOffset = this.writeInt16Array(byteOffset, value); + byteOffset = writer.writeInt16Array(byteOffset, value); } else if (vr === 'UL') { - byteOffset = this.writeUint32Array(byteOffset, value); + byteOffset = writer.writeUint32Array(byteOffset, value); } else if (vr === 'SL') { - byteOffset = this.writeInt32Array(byteOffset, value); + byteOffset = writer.writeInt32Array(byteOffset, value); } else if (vr === 'FL') { - byteOffset = this.writeFloat32Array(byteOffset, value); + byteOffset = writer.writeFloat32Array(byteOffset, value); } else if (vr === 'FD') { - byteOffset = this.writeFloat64Array(byteOffset, value); + byteOffset = writer.writeFloat64Array(byteOffset, value); } else if (vr === 'SQ') { - byteOffset = this.writeDataElementItems(byteOffset, value, isImplicit); + byteOffset = this.writeDataElementItems( + writer, byteOffset, value, isImplicit); } else if (vr === 'AT') { for (var i = 0; i < value.length; ++i) { var hexString = value[i] + ''; @@ -554,10 +410,20 @@ dwv.dicom.DataWriter.prototype.writeDataElementValue = function ( var dec1 = parseInt(hexString1, 16); var dec2 = parseInt(hexString2, 16); var atValue = new Uint16Array([dec1, dec2]); - byteOffset = this.writeUint16Array(byteOffset, atValue); + byteOffset = writer.writeUint16Array(byteOffset, atValue); } } else { - byteOffset = this.writeStringArray(byteOffset, value); + // join if array + if (Array.isArray(value)) { + value = value.join('\\'); + } + // write + if (vr === 'SH' || vr === 'LO' || vr === 'ST' || + vr === 'PN' || vr === 'LT' || vr === 'UT') { + byteOffset = writer.writeSpecialString(byteOffset, value); + } else { + byteOffset = writer.writeString(byteOffset, value); + } } } // return new offset @@ -567,6 +433,7 @@ dwv.dicom.DataWriter.prototype.writeDataElementValue = function ( /** * Write a pixel data element. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {string} vr The data Value Representation (VR). * @param {string} vl The data Value Length (VL). * @param {number} byteOffset The offset to start writing from. @@ -574,8 +441,8 @@ dwv.dicom.DataWriter.prototype.writeDataElementValue = function ( * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( - vr, vl, byteOffset, value, isImplicit) { +dwv.dicom.DicomWriter.prototype.writePixelDataElementValue = function ( + writer, vr, vl, byteOffset, value, isImplicit) { // explicit length if (vl !== 'u/l') { var finalValue = value[0]; @@ -585,7 +452,7 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( } // write byteOffset = this.writeDataElementValue( - vr, vl, byteOffset, finalValue, isImplicit); + writer, vr, vl, byteOffset, finalValue, isImplicit); } else { // pixel data as sequence var item = {}; @@ -614,7 +481,8 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( }; } // write - byteOffset = this.writeDataElementItems(byteOffset, [item], isImplicit); + byteOffset = this.writeDataElementItems( + writer, byteOffset, [item], isImplicit); } // return new offset @@ -624,32 +492,33 @@ dwv.dicom.DataWriter.prototype.writePixelDataElementValue = function ( /** * Write a data element. * + * @param {dwv.dicom.DataWriter} writer The raw data writer. * @param {object} element The DICOM data element to write. * @param {number} byteOffset The offset to start writing from. * @param {boolean} isImplicit Is the DICOM VR implicit? * @returns {number} The new offset position. */ -dwv.dicom.DataWriter.prototype.writeDataElement = function ( - element, byteOffset, isImplicit) { - var isTagWithVR = dwv.dicom.isTagWithVR( - element.tag.group, element.tag.element); +dwv.dicom.DicomWriter.prototype.writeDataElement = function ( + writer, element, byteOffset, isImplicit) { + var isTagWithVR = new dwv.dicom.Tag( + element.tag.group, element.tag.element).isWithVR(); var is32bitVLVR = (isImplicit || !isTagWithVR) ? true : dwv.dicom.is32bitVLVR(element.vr); // group - byteOffset = this.writeHex(byteOffset, element.tag.group); + byteOffset = writer.writeHex(byteOffset, element.tag.group); // element - byteOffset = this.writeHex(byteOffset, element.tag.element); + byteOffset = writer.writeHex(byteOffset, element.tag.element); // VR var vr = element.vr; // use VR=UN for private sequence if (this.useUnVrForPrivateSq && - dwv.dicom.isPrivateGroup(element.tag.group) && + new dwv.dicom.Tag(element.tag.group, element.tag.element).isPrivate() && vr === 'SQ') { dwv.logger.warn('Write element using VR=UN for private sequence.'); vr = 'UN'; } if (isTagWithVR && !isImplicit) { - byteOffset = this.writeString(byteOffset, vr); + byteOffset = writer.writeString(byteOffset, vr); // reserved 2 bytes for 32bit VL if (is32bitVLVR) { byteOffset += 2; @@ -665,9 +534,9 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( } // VL if (is32bitVLVR) { - byteOffset = this.writeUint32(byteOffset, vl); + byteOffset = writer.writeUint32(byteOffset, vl); } else { - byteOffset = this.writeUint16(byteOffset, vl); + byteOffset = writer.writeUint16(byteOffset, vl); } // value @@ -679,10 +548,10 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( // write if (element.tag.name === 'x7FE00010') { byteOffset = this.writePixelDataElementValue( - element.vr, element.vl, byteOffset, value, isImplicit); + writer, element.vr, element.vl, byteOffset, value, isImplicit); } else { byteOffset = this.writeDataElementValue( - element.vr, element.vl, byteOffset, value, isImplicit); + writer, element.vr, element.vl, byteOffset, value, isImplicit); } // sequence delimitation item for sequence with implicit length @@ -698,175 +567,14 @@ dwv.dicom.DataWriter.prototype.writeDataElement = function ( vl: 0, value: [] }; - byteOffset = this.writeDataElement(seqDelimElement, byteOffset, isImplicit); + byteOffset = this.writeDataElement( + writer, seqDelimElement, byteOffset, isImplicit); } // return new offset return byteOffset; }; -/** - * Is this element an implicit length sequence? - * - * @param {object} element The element to check. - * @returns {boolean} True if it is. - */ -dwv.dicom.isImplicitLengthSequence = function (element) { - // sequence with no length - return (element.vr === 'SQ') && - (element.vl === 'u/l'); -}; - -/** - * Is this element an implicit length item? - * - * @param {object} element The element to check. - * @returns {boolean} True if it is. - */ -dwv.dicom.isImplicitLengthItem = function (element) { - // item with no length - return (element.tag.name === 'xFFFEE000') && - (element.vl === 'u/l'); -}; - -/** - * Is this element an implicit length pixel data? - * - * @param {object} element The element to check. - * @returns {boolean} True if it is. - */ -dwv.dicom.isImplicitLengthPixels = function (element) { - // pixel data with no length - return (element.tag.name === 'x7FE00010') && - (element.vl === 'u/l'); -}; - -/** - * Helper method to flatten an array of typed arrays to 2D typed array - * - * @param {Array} initialArray array of typed arrays - * @returns {object} a typed array containing all values - */ -dwv.dicom.flattenArrayOfTypedArrays = function (initialArray) { - var initialArrayLength = initialArray.length; - var arrayLength = initialArray[0].length; - // If this is not a array of arrays, just return the initial one: - if (typeof arrayLength === 'undefined') { - return initialArray; - } - - var flattenendArrayLength = initialArrayLength * arrayLength; - - var flattenedArray = new initialArray[0].constructor(flattenendArrayLength); - - for (var i = 0; i < initialArrayLength; i++) { - var indexFlattenedArray = i * arrayLength; - flattenedArray.set(initialArray[i], indexFlattenedArray); - } - return flattenedArray; -}; - -/** - * DICOM writer. - * - * @class - */ -dwv.dicom.DicomWriter = function () { - - // flag to use VR=UN for private sequences, default to false - // (mainly used in tests) - this.useUnVrForPrivateSq = false; - - // possible tag actions - var actions = { - copy: function (item) { - return item; - }, - remove: function () { - return null; - }, - clear: function (item) { - item.value[0] = ''; - item.vl = 0; - item.endOffset = item.startOffset; - return item; - }, - replace: function (item, value) { - var paddedValue = dwv.dicom.padElementValue(item, value); - item.value[0] = paddedValue; - item.vl = paddedValue.length; - item.endOffset = item.startOffset + paddedValue.length; - return item; - } - }; - - // default rules: just copy - var defaultRules = { - default: {action: 'copy', value: null} - }; - - /** - * Public (modifiable) rules. - * Set of objects as: - * name : { action: 'actionName', value: 'optionalValue } - * The names are either 'default', tagName or groupName. - * Each DICOM element will be checked to see if a rule is applicable. - * First checked by tagName and then by groupName, - * if nothing is found the default rule is applied. - */ - this.rules = defaultRules; - - /** - * Example anonymisation rules. - */ - this.anonymisationRules = { - default: {action: 'remove', value: null}, - PatientName: {action: 'replace', value: 'Anonymized'}, // tag - 'Meta Element': {action: 'copy', value: null}, // group 'x0002' - Acquisition: {action: 'copy', value: null}, // group 'x0018' - 'Image Presentation': {action: 'copy', value: null}, // group 'x0028' - Procedure: {action: 'copy', value: null}, // group 'x0040' - 'Pixel Data': {action: 'copy', value: null} // group 'x7fe0' - }; - - /** - * Get the element to write according to the class rules. - * Priority order: tagName, groupName, default. - * - * @param {object} element The element to check - * @returns {object} The element to write, can be null. - */ - this.getElementToWrite = function (element) { - // get group and tag string name - var tagName = null; - var dict = dwv.dicom.dictionary; - var group = element.tag.group; - var groupName = dwv.dicom.TagGroups[group.substr(1)]; // remove first 0 - - if (typeof dict[group] !== 'undefined' && - typeof dict[group][element.tag.element] !== 'undefined') { - tagName = dict[group][element.tag.element][2]; - } - // apply rules: - var rule; - if (typeof this.rules[element.tag.name] !== 'undefined') { - // 1. tag itself - rule = this.rules[element.tag.name]; - } else if (tagName !== null && typeof this.rules[tagName] !== 'undefined') { - // 2. tag name - rule = this.rules[tagName]; - } else if (typeof this.rules[groupName] !== 'undefined') { - // 3. group name - rule = this.rules[groupName]; - } else { - // 4. default - rule = this.rules['default']; - } - // apply action on element and return - return actions[rule.action](element, rule.value); - }; -}; - /** * Get the ArrayBuffer corresponding to input DICOM elements. * @@ -925,11 +633,6 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { var realVl = element.endOffset - element.startOffset; localSize += parseInt(realVl, 10); - // add size of pixel sequence delimitation items - if (dwv.dicom.isImplicitLengthPixels(element)) { - localSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); - } - // sort elements if (groupName === 'Meta Element') { metaElements.push(element); @@ -972,14 +675,20 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { var buffer = new ArrayBuffer(totalSize); var metaWriter = new dwv.dicom.DataWriter(buffer); var dataWriter = new dwv.dicom.DataWriter(buffer, !isBigEndian); + // special character set + if (typeof dicomElements.x00080005 !== 'undefined') { + var scs = dwv.dicom.cleanString(dicomElements.x00080005.value[0]); + dataWriter.setUtfLabel(dwv.dicom.getUtfLabel(scs)); + } + var offset = 128; // DICM offset = metaWriter.writeString(offset, 'DICM'); // FileMetaInformationGroupLength - offset = metaWriter.writeDataElement(fmigl, offset, false); + offset = this.writeDataElement(metaWriter, fmigl, offset, false); // write meta for (var j = 0, lenj = metaElements.length; j < lenj; ++j) { - offset = metaWriter.writeDataElement(metaElements[j], offset, false); + offset = this.writeDataElement(metaWriter, metaElements[j], offset, false); } // check meta position @@ -988,21 +697,22 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { if (offset !== metaOffset) { dwv.logger.warn('Bad size calculation... meta offset: ' + offset + ', calculated size:' + metaOffset + - '(diff:', offset - metaOffset, ')'); + ' (diff:' + (offset - metaOffset) + ')'); } // pass flag to writer dataWriter.useUnVrForPrivateSq = this.useUnVrForPrivateSq; // write non meta for (var k = 0, lenk = rawElements.length; k < lenk; ++k) { - offset = dataWriter.writeDataElement(rawElements[k], offset, isImplicit); + offset = this.writeDataElement( + dataWriter, rawElements[k], offset, isImplicit); } // check final position if (offset !== totalSize) { dwv.logger.warn('Bad size calculation... final offset: ' + offset + ', calculated size:' + totalSize + - '(diff:', offset - totalSize, ')'); + ' (diff:' + (offset - totalSize) + ')'); } // return return buffer; @@ -1015,16 +725,14 @@ dwv.dicom.DicomWriter.prototype.getBuffer = function (dicomElements) { * @param {object} element The DICOM element. */ dwv.dicom.checkUnknownVR = function (element) { - var dict = dwv.dicom.dictionary; if (element.vr === 'UN') { - if (typeof dict[element.tag.group] !== 'undefined' && - typeof dict[element.tag.group][element.tag.element] !== 'undefined') { - if (element.vr !== dict[element.tag.group][element.tag.element][0]) { - element.vr = dict[element.tag.group][element.tag.element][0]; - dwv.logger.info('Element ' + element.tag.group + - ' ' + element.tag.element + - ' VR changed from UN to ' + element.vr); - } + var tag = new dwv.dicom.Tag(element.tag.group, element.tag.element); + var dictVr = tag.getVrFromDictionary(); + if (dictVr !== null && element.vr !== dictVr) { + element.vr = dictVr; + dwv.logger.info('Element ' + element.tag.group + + ' ' + element.tag.element + + ' VR changed from UN to ' + element.vr); } } }; @@ -1036,12 +744,11 @@ dwv.dicom.checkUnknownVR = function (element) { * @returns {object} The DICOM element. */ dwv.dicom.getDicomElement = function (tagName) { - var tagGE = dwv.dicom.getGroupElementFromName(tagName); - var dict = dwv.dicom.dictionary; + var tag = dwv.dicom.getTagFromDictionary(tagName); // return element definition return { - tag: {group: tagGE.group, element: tagGE.element}, - vr: dict[tagGE.group][tagGE.element][0] + tag: {group: tag.getGroup(), element: tag.getElement()}, + vr: tag.getVrFromDictionary() }; }; @@ -1095,8 +802,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { subSize += dwv.dicom.setElementValue( subElement, itemData[elemKeys[j]]); - name = dwv.dicom.getGroupElementKey( - subElement.tag.group, subElement.tag.element); + name = new dwv.dicom.Tag( + subElement.tag.group, subElement.tag.element).getKey(); itemElements[name] = subElement; subSize += dwv.dicom.getDataElementPrefixByteSize( subElement.vr, isImplicit); @@ -1109,8 +816,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { vl: (explicitLength ? subSize : 'u/l'), value: [] }; - name = dwv.dicom.getGroupElementKey( - itemElement.tag.group, itemElement.tag.element); + name = new dwv.dicom.Tag( + itemElement.tag.group, itemElement.tag.element).getKey(); itemElements[name] = itemElement; subSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); @@ -1122,8 +829,8 @@ dwv.dicom.setElementValue = function (element, value, isImplicit) { vl: 0, value: [] }; - name = dwv.dicom.getGroupElementKey( - itemDelimElement.tag.group, itemDelimElement.tag.element); + name = new dwv.dicom.Tag( + itemDelimElement.tag.group, itemDelimElement.tag.element).getKey(); itemElements[name] = itemDelimElement; subSize += dwv.dicom.getDataElementPrefixByteSize('NONE', isImplicit); } diff --git a/src/gui/drawLayer.js b/src/gui/drawLayer.js index 6e48b25a55..17d360103f 100644 --- a/src/gui/drawLayer.js +++ b/src/gui/drawLayer.js @@ -1,5 +1,6 @@ // namespaces var dwv = dwv || {}; +/** @namespace */ dwv.gui = dwv.gui || {}; /** @@ -13,7 +14,8 @@ var Konva = Konva || {}; /** * Draw layer. * - * @param {object} containerDiv The layer div. + * @param {HTMLElement} containerDiv The layer div, its id will be used + * as this layer id. * @class */ dwv.gui.DrawLayer = function (containerDiv) { @@ -21,18 +23,45 @@ dwv.gui.DrawLayer = function (containerDiv) { // specific css class name containerDiv.className += ' drawLayer'; + // closure to self + var self = this; + // konva stage var konvaStage = null; // konva layer var konvaLayer; /** - * The layer size as {x,y}. + * The layer base size as {x,y}. + * + * @private + * @type {object} + */ + var baseSize; + + /** + * The layer base spacing as {x,y}. + * + * @private + * @type {object} + */ + var baseSpacing; + + /** + * The layer fit scale. + * + * @private + * @type {object} + */ + var fitScale = {x: 1, y: 1}; + + /** + * The base layer offset. * * @private * @type {object} */ - var layerSize; + var baseOffset = {x: 0, y: 0}; /** * The draw controller. @@ -42,6 +71,31 @@ dwv.gui.DrawLayer = function (containerDiv) { */ var drawController = null; + /** + * The plane helper. + * + * @private + * @type {object} + */ + var planeHelper; + + /** + * The associated data index. + * + * @private + * @type {number} + */ + var dataIndex = null; + + /** + * Get the associated data index. + * + * @returns {number} The index. + */ + this.getDataIndex = function () { + return dataIndex; + }; + /** * Listener handler. * @@ -77,15 +131,45 @@ dwv.gui.DrawLayer = function (containerDiv) { return drawController; }; + /** + * Set the plane helper. + * + * @param {object} helper The helper. + */ + this.setPlaneHelper = function (helper) { + planeHelper = helper; + }; + // common layer methods [start] --------------- /** - * Get the layer size. + * Get the id of the layer. + * + * @returns {string} The string id. + */ + this.getId = function () { + return containerDiv.id; + }; + + /** + * Get the data full size, ie size * spacing. + * + * @returns {object} The full size as {x,y}. + */ + this.getFullSize = function () { + return { + x: baseSize.x * baseSpacing.x, + y: baseSize.y * baseSpacing.y + }; + }; + + /** + * Get the layer base size (without scale). * * @returns {object} The size as {x,y}. */ - this.getSize = function () { - return layerSize; + this.getBaseSize = function () { + return baseSize; }; /** @@ -112,9 +196,14 @@ dwv.gui.DrawLayer = function (containerDiv) { * @param {object} newScale The scale as {x,y}. */ this.setScale = function (newScale) { - konvaStage.scale(newScale); - // update labels - updateLabelScale(newScale); + var orientedNewScale = planeHelper.getOrientedXYZ(newScale); + var fullScale = { + x: fitScale.x * orientedNewScale.x, + y: fitScale.y * orientedNewScale.y + }; + konvaStage.scale(fullScale); + // update labelss + updateLabelScale(fullScale); }; /** @@ -123,29 +212,29 @@ dwv.gui.DrawLayer = function (containerDiv) { * @param {object} newOffset The offset as {x,y}. */ this.setOffset = function (newOffset) { - konvaStage.offset(newOffset); + var planeNewOffset = planeHelper.getPlaneOffsetFromOffset3D(newOffset); + konvaStage.offset({ + x: baseOffset.x + planeNewOffset.x, + y: baseOffset.y + planeNewOffset.y + }); }; /** - * Set the layer z-index. + * Set the base layer offset. Resets the layer offset. * - * @param {number} index The index. + * @param {object} off The offset as {x,y}. */ - this.setZIndex = function (index) { - containerDiv.style.zIndex = index; - }; - - /** - * Resize the layer: update the window scale and layer sizes. - * - * @param {object} newScale The layer scale as {x,y}. - */ - this.resize = function (newScale) { - // resize stage - konvaStage.setWidth(parseInt(layerSize.x * newScale.x, 10)); - konvaStage.setHeight(parseInt(layerSize.y * newScale.y, 10)); - // set scale - this.setScale(newScale); + this.setBaseOffset = function (off) { + baseOffset = planeHelper.getPlaneOffsetFromOffset3D({ + x: off.getX(), + y: off.getY(), + z: off.getZ() + }); + // reset offset + konvaStage.offset({ + x: baseOffset.x, + y: baseOffset.y + }); }; /** @@ -177,22 +266,21 @@ dwv.gui.DrawLayer = function (containerDiv) { /** * Initialise the layer: set the canvas and context * - * @param {object} image The image. - * @param {object} _metaData The image meta data. - */ - this.initialise = function (image, _metaData) { - // get sizes - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; + * @param {object} size The image size as {x,y}. + * @param {object} spacing The image spacing as {x,y}. + * @param {number} index The associated data index. + */ + this.initialise = function (size, spacing, index) { + // set locals + baseSize = size; + baseSpacing = spacing; + dataIndex = index; // create stage konvaStage = new Konva.Stage({ container: containerDiv, - width: layerSize.x, - height: layerSize.y, + width: baseSize.x, + height: baseSize.y, listening: false }); // reset style @@ -207,22 +295,34 @@ dwv.gui.DrawLayer = function (containerDiv) { konvaStage.add(konvaLayer); // create draw controller - drawController = new dwv.DrawController(konvaLayer); + drawController = new dwv.ctrl.DrawController(konvaLayer); }; /** - * Update the layer position. + * Fit the layer to its parent container. * - * @param {object} pos The new position. + * @param {number} fitScale1D The 1D fit scale. */ - this.updatePosition = function (pos) { - this.getDrawController().activateDrawLayer(pos[0], pos[1]); + this.fitToContainer = function (fitScale1D) { + // update fit scale + fitScale = { + x: fitScale1D * baseSpacing.x, + y: fitScale1D * baseSpacing.y + }; + // update konva + var fullSize = this.getFullSize(); + var width = Math.floor(fullSize.x * fitScale1D); + var height = Math.floor(fullSize.y * fitScale1D); + konvaStage.setWidth(width); + konvaStage.setHeight(height); + // reset scale + this.setScale({x: 1, y: 1, z: 1}); }; /** - * Activate the layer: propagate events. + * Enable and listen to container interaction events. */ - this.activate = function () { + this.bindInteraction = function () { konvaStage.listening(true); // allow pointer events containerDiv.style.pointerEvents = 'auto'; @@ -234,9 +334,9 @@ dwv.gui.DrawLayer = function (containerDiv) { }; /** - * Deactivate the layer: stop propagating events. + * Disable and stop listening to container interaction events. */ - this.deactivate = function () { + this.unbindInteraction = function () { konvaStage.listening(false); // disable pointer events containerDiv.style.pointerEvents = 'none'; @@ -247,6 +347,17 @@ dwv.gui.DrawLayer = function (containerDiv) { } }; + /** + * Set the current position. + * + * @param {dwv.math.Point} position The new position. + * @param {dwv.math.Index} index The new index. + */ + this.setCurrentPosition = function (position, index) { + this.getDrawController().activateDrawLayer( + index, planeHelper.getScrollIndex()); + }; + /** * Add an event listener to this class. * @@ -276,6 +387,8 @@ dwv.gui.DrawLayer = function (containerDiv) { * @private */ function fireEvent(event) { + event.srclayerid = self.getId(); + event.dataindex = dataIndex; listenerHandler.fireEvent(event); } @@ -285,7 +398,7 @@ dwv.gui.DrawLayer = function (containerDiv) { * Update label scale: compensate for it so * that label size stays visually the same. * - * @param {object} scale The scale to compensate for + * @param {object} scale The scale to compensate for as {x,y}. */ function updateLabelScale(scale) { // same formula as in style::applyZoomScale: diff --git a/src/gui/generic.js b/src/gui/generic.js index 7d7b43b640..4581e87169 100644 --- a/src/gui/generic.js +++ b/src/gui/generic.js @@ -1,7 +1,6 @@ // namespaces var dwv = dwv || {}; dwv.gui = dwv.gui || {}; -dwv.gui.base = dwv.gui.base || {}; /** * List of interaction event names. @@ -24,8 +23,9 @@ dwv.gui.interactionEventNames = [ * @param {number} containerDivId The id of the container div. * @param {string} name The name or id to find. * @returns {object} The found element or null. + * @deprecated */ -dwv.gui.base.getElement = function (containerDivId, name) { +dwv.gui.getElement = function (containerDivId, name) { // get by class in the container div var parent = document.getElementById(containerDivId); if (!parent) { @@ -43,31 +43,40 @@ dwv.gui.base.getElement = function (containerDivId, name) { }; /** - * Get the size available for a div. + * Get a HTML element associated to a container div. Defaults to local one. * - * @param {object} div The input div. - * @returns {object} The available width and height as {x,y}. + * @see dwv.gui.getElement + * @deprecated */ -dwv.gui.getDivSize = function (div) { - var parent = div.parentNode; - // offsetHeight: height of an element, including vertical padding - // and borders - // ref: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight - var height = parent.offsetHeight; - // remove the height of other elements of the container div - var kids = parent.children; - for (var i = 0; i < kids.length; ++i) { - if (!kids[i].classList.contains(div.className)) { - var styles = window.getComputedStyle(kids[i]); - // offsetHeight does not include margin - var margin = parseFloat(styles.getPropertyValue('margin-top'), 10) + - parseFloat(styles.getPropertyValue('margin-bottom'), 10); - height -= (kids[i].offsetHeight + margin); - } - } - return {x: parent.offsetWidth, y: height}; +dwv.getElement = dwv.gui.getElement; + +/** + * Prompt the user for some text. Uses window.prompt. + * + * @param {string} message The message in front of the input field. + * @param {string} value The input default value. + * @returns {string} The new value. + */ +dwv.gui.prompt = function (message, value) { + return prompt(message, value); }; +/** + * Prompt the user for some text. Defaults to local one. + * + * @see dwv.gui.prompt + */ +dwv.prompt = dwv.gui.prompt; + +/** + * Open a dialogue to edit roi data. Defaults to undefined. + * + * @param {object} data The roi data. + * @param {Function} callback The callback to launch on dialogue exit. + * @see dwv.tool.Draw + */ +dwv.openRoiDialog; + /** * Get the positions (without the parent offset) of a list of touch events. * @@ -117,14 +126,18 @@ dwv.gui.getEventOffset = function (event) { // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/targetTouches positions = dwv.gui.getTouchesPositions(event.targetTouches); } else if (typeof event.changedTouches !== 'undefined' && - event.changedTouches.length !== 0) { + event.changedTouches.length !== 0) { // see https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/changedTouches positions = dwv.gui.getTouchesPositions(event.changedTouches); } else { - // layerX is used by Firefox - var ex = event.offsetX === undefined ? event.layerX : event.offsetX; - var ey = event.offsetY === undefined ? event.layerY : event.offsetY; - positions.push({x: ex, y: ey}); + // offsetX/Y: the offset in the X coordinate of the mouse pointer + // between that event and the padding edge of the target node + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX + // https://caniuse.com/mdn-api_mouseevent_offsetx + positions.push({ + x: event.offsetX, + y: event.offsetY + }); } return positions; }; diff --git a/src/gui/layerGroup.js b/src/gui/layerGroup.js new file mode 100644 index 0000000000..9096c1d71e --- /dev/null +++ b/src/gui/layerGroup.js @@ -0,0 +1,650 @@ +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; + +/** + * Get the layer group div id. + * + * @param {number} groupId The layer group id. + * @param {number} layerId The lyaer id. + * @returns {string} A string id. + */ +dwv.gui.getLayerGroupDivId = function (groupId, layerId) { + return 'layer-' + groupId + '-' + layerId; +}; + +/** + * Get the layer details from a div id. + * + * @param {string} idString The layer group id. + * @returns {object} The layer details as {groupId, layerId}. + */ +dwv.gui.getLayerDetailsFromLayerDivId = function (idString) { + var posHyphen = idString.lastIndexOf('-'); + var groupId = null; + var layerId = null; + if (posHyphen !== -1) { + groupId = parseInt(idString.substring(6, posHyphen), 10); + layerId = parseInt(idString.substring(posHyphen + 1), 10); + } + return { + groupId: groupId, + layerId: layerId + }; +}; + +/** + * Get the layer details from a mouse event. + * + * @param {object} event The event to get the layer div id from. Expecting + * an event origininating from a canvas inside a layer HTML div + * with the 'layer' class and id generated with `dwv.gui.getLayerGroupDivId`. + * @returns {object} The layer details as {groupId, layerId}. + */ +dwv.gui.getLayerDetailsFromEvent = function (event) { + var res = null; + // get the closest element from the event target and with the 'layer' class + var layerDiv = event.target.closest('.layer'); + if (layerDiv && typeof layerDiv.id !== 'undefined') { + res = dwv.gui.getLayerDetailsFromLayerDivId(layerDiv.id); + } + return res; +}; + +/** + * Get a view orientation according to an image geometry (with its orientation) + * and target orientation. + * + * @param {dwv.image.Geometry} imageGeometry The image geometry. + * @param {dwv.math.Matrix33} targetOrientation The target orientation. + * @returns {dwv.math.Matrix33} The view orientation. + */ +dwv.gui.getViewOrientation = function (imageGeometry, targetOrientation) { + var viewOrientation = dwv.math.getIdentityMat33(); + if (typeof targetOrientation !== 'undefined') { + // image orientation as one and zeros + // -> view orientation is one and zeros + var imgOrientation = imageGeometry.getOrientation().asOneAndZeros(); + // imgOrientation * viewOrientation = targetOrientation + // -> viewOrientation = inv(imgOrientation) * targetOrientation + viewOrientation = + imgOrientation.getInverse().multiply(targetOrientation); + } + return viewOrientation; +}; + +/** + * Layer group. + * + * Display position: {x,y} + * Plane position: Index (access: get(i)) + * (world) Position: Point3D (access: getX, getY, getZ) + * + * Display -> World: + * planePos = viewLayer.displayToPlanePos(displayPos) + * -> compensate for layer scale and offset + * pos = viewController.getPositionFromPlanePoint(planePos) + * + * World -> display + * planePos = viewController.getOffset3DFromPlaneOffset(pos) + * no need yet for a planePos to displayPos... + * + * @param {object} containerDiv The associated HTML div. + * @param {number} groupId The group id. + * @class + */ +dwv.gui.LayerGroup = function (containerDiv, groupId) { + + // closure to self + var self = this; + // list of layers + var layers = []; + + /** + * The layer scale as {x,y}. + * + * @private + * @type {object} + */ + var scale = {x: 1, y: 1, z: 1}; + + /** + * The base scale as {x,y}: all posterior scale will be on top of this one. + * + * @private + * @type {object} + */ + var baseScale = {x: 1, y: 1, z: 1}; + + /** + * The layer offset as {x,y}. + * + * @private + * @type {object} + */ + var offset = {x: 0, y: 0, z: 0}; + + /** + * Active view layer index. + * + * @private + * @type {number} + */ + var activeViewLayerIndex = null; + + /** + * Active draw layer index. + * + * @private + * @type {number} + */ + var activeDrawLayerIndex = null; + + /** + * Listener handler. + * + * @type {object} + * @private + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * The target orientation matrix. + * + * @type {object} + * @private + */ + var targetOrientation; + + /** + * Get the target orientation. + * + * @returns {dwv.math.Matrix33} The orientation matrix. + */ + this.getTargetOrientation = function () { + return targetOrientation; + }; + + /** + * Set the target orientation. + * + * @param {dwv.math.Matrix33} orientation The orientation matrix. + */ + this.setTargetOrientation = function (orientation) { + targetOrientation = orientation; + }; + + /** + * Get the Id of the container div. + * + * @returns {string} The id of the div. + */ + this.getElementId = function () { + return containerDiv.id; + }; + + /** + * Get the layer group id. + * + * @returns {number} The id. + */ + this.getGroupId = function () { + return groupId; + }; + + /** + * Get the layer scale. + * + * @returns {object} The scale as {x,y,z}. + */ + this.getScale = function () { + return scale; + }; + + /** + * Get the base scale. + * + * @returns {object} The scale as {x,y,z}. + */ + this.getBaseScale = function () { + return baseScale; + }; + + /** + * Get the added scale: the scale added to the base scale + * + * @returns {object} The scale as {x,y,z}. + */ + this.getAddedScale = function () { + return { + x: scale.x / baseScale.x, + y: scale.y / baseScale.y, + z: scale.z / baseScale.z + }; + }; + + /** + * Get the layer offset. + * + * @returns {object} The offset as {x,y,z}. + */ + this.getOffset = function () { + return offset; + }; + + /** + * Get the number of layers handled by this class. + * + * @returns {number} The number of layers. + */ + this.getNumberOfLayers = function () { + return layers.length; + }; + + /** + * Get the active image layer. + * + * @returns {object} The layer. + */ + this.getActiveViewLayer = function () { + return layers[activeViewLayerIndex]; + }; + + /** + * Get the view layers associated to a data index. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getViewLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.ViewLayer && + layers[i].getDataIndex() === index) { + res.push(layers[i]); + } + } + return res; + }; + + /** + * Get the active draw layer. + * + * @returns {object} The layer. + */ + this.getActiveDrawLayer = function () { + return layers[activeDrawLayerIndex]; + }; + + /** + * Get the draw layers associated to a data index. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getDrawLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.DrawLayer && + layers[i].getDataIndex() === index) { + res.push(layers[i]); + } + } + return res; + }; + + /** + * Set the active view layer. + * + * @param {number} index The index of the layer to set as active. + */ + this.setActiveViewLayer = function (index) { + activeViewLayerIndex = index; + }; + + /** + * Set the active view layer with a data index. + * + * @param {number} index The data index. + */ + this.setActiveViewLayerByDataIndex = function (index) { + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.ViewLayer && + layers[i].getDataIndex() === index) { + this.setActiveViewLayer(i); + break; + } + } + }; + + /** + * Set the active draw layer. + * + * @param {number} index The index of the layer to set as active. + */ + this.setActiveDrawLayer = function (index) { + activeDrawLayerIndex = index; + }; + + /** + * Set the active draw layer with a data index. + * + * @param {number} index The data index. + */ + this.setActiveDrawLayerByDataIndex = function (index) { + for (var i = 0; i < layers.length; ++i) { + if (layers[i] instanceof dwv.gui.DrawLayer && + layers[i].getDataIndex() === index) { + this.setActiveDrawLayer(i); + break; + } + } + }; + + /** + * Add a view layer. + * + * @returns {object} The created layer. + */ + this.addViewLayer = function () { + // layer index + var viewLayerIndex = layers.length; + // create div + var div = getNextLayerDiv(); + // prepend to container + containerDiv.append(div); + // view layer + var layer = new dwv.gui.ViewLayer(div); + // add layer + layers.push(layer); + // mark it as active + this.setActiveViewLayer(viewLayerIndex); + // bind view layer events + bindViewLayer(layer); + // return + return layer; + }; + + /** + * Add a draw layer. + * + * @returns {object} The created layer. + */ + this.addDrawLayer = function () { + // store active index + activeDrawLayerIndex = layers.length; + // create div + var div = getNextLayerDiv(); + // prepend to container + containerDiv.append(div); + // draw layer + var layer = new dwv.gui.DrawLayer(div); + // add layer + layers.push(layer); + // return + return layer; + }; + + /** + * Bind view layer events to this. + * + * @param {object} viewLayer The view layer to bind. + */ + function bindViewLayer(viewLayer) { + // listen to position change to update other group layers + viewLayer.addEventListener( + 'positionchange', self.updateLayersToPositionChange); + // propagate view viewLayer-layer events + for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { + viewLayer.addEventListener(dwv.image.viewEventNames[j], fireEvent); + } + // propagate viewLayer events + viewLayer.addEventListener('renderstart', fireEvent); + viewLayer.addEventListener('renderend', fireEvent); + } + + /** + * Get the next layer DOM div. + * + * @returns {HTMLElement} A DOM div. + */ + function getNextLayerDiv() { + var div = document.createElement('div'); + div.id = dwv.gui.getLayerGroupDivId(groupId, layers.length); + div.className = 'layer'; + div.style.pointerEvents = 'none'; + return div; + } + + /** + * Empty the layer list. + */ + this.empty = function () { + layers = []; + // reset active indices + activeViewLayerIndex = null; + activeDrawLayerIndex = null; + // clean container div + var previous = containerDiv.getElementsByClassName('layer'); + if (previous) { + while (previous.length > 0) { + previous[0].remove(); + } + } + }; + + /** + * Update layers (but not the active view layer) to a position change. + * + * @param {object} event The position change event. + */ + this.updateLayersToPositionChange = function (event) { + // pause positionchange listeners + for (var j = 0; j < layers.length; ++j) { + if (layers[j] instanceof dwv.gui.ViewLayer) { + layers[j].removeEventListener( + 'positionchange', self.updateLayersToPositionChange); + layers[j].removeEventListener('positionchange', fireEvent); + } + } + + var index = new dwv.math.Index(event.value[0]); + var position = new dwv.math.Point(event.value[1]); + // update position for all layers except the source one + for (var i = 0; i < layers.length; ++i) { + if (layers[i].getId() !== event.srclayerid) { + layers[i].setCurrentPosition(position, index); + } + } + + // re-start positionchange listeners + for (var k = 0; k < layers.length; ++k) { + if (layers[k] instanceof dwv.gui.ViewLayer) { + layers[k].addEventListener( + 'positionchange', self.updateLayersToPositionChange); + layers[k].addEventListener('positionchange', fireEvent); + } + } + }; + + /** + * Fit the display to the size of the container. + * To be called once the image is loaded. + */ + this.fitToContainer = function () { + // check container size + if (containerDiv.offsetWidth === 0 && + containerDiv.offsetHeight === 0) { + throw new Error('Cannot fit to zero sized container.'); + } + // find best fit + var fitScales = []; + for (var i = 0; i < layers.length; ++i) { + var fullSize = layers[i].getFullSize(); + fitScales.push(containerDiv.offsetWidth / fullSize.x); + fitScales.push(containerDiv.offsetHeight / fullSize.y); + } + var fitScale = Math.min.apply(null, fitScales); + // apply to layers + for (var j = 0; j < layers.length; ++j) { + layers[j].fitToContainer(fitScale); + } + }; + + /** + * Add scale to the layers. Scale cannot go lower than 0.1. + * + * @param {number} scaleStep The scale to add. + * @param {dwv.math.Point3D} center The scale center Point3D. + */ + this.addScale = function (scaleStep, center) { + var newScale = { + x: scale.x * (1 + scaleStep), + y: scale.y * (1 + scaleStep), + z: scale.z * (1 + scaleStep) + }; + var centerPlane = { + x: (center.getX() - offset.x) * scale.x, + y: (center.getY() - offset.y) * scale.y, + z: (center.getZ() - offset.z) * scale.z + }; + // center should stay the same: + // center / newScale + newOffset = center / oldScale + oldOffset + // => newOffset = center / oldScale + oldOffset - center / newScale + var newOffset = { + x: (centerPlane.x / scale.x) + offset.x - (centerPlane.x / newScale.x), + y: (centerPlane.y / scale.y) + offset.y - (centerPlane.y / newScale.y), + z: (centerPlane.z / scale.z) + offset.z - (centerPlane.z / newScale.z) + }; + + this.setOffset(newOffset); + this.setScale(newScale); + }; + + /** + * Set the layers' scale. + * + * @param {object} newScale The scale to apply as {x,y,z}. + * @fires dwv.ctrl.LayerGroup#zoomchange + */ + this.setScale = function (newScale) { + scale = newScale; + // apply to layers + for (var i = 0; i < layers.length; ++i) { + layers[i].setScale(scale); + } + + /** + * Zoom change event. + * + * @event dwv.ctrl.LayerGroup#zoomchange + * @type {object} + * @property {Array} value The changed value. + */ + fireEvent({ + type: 'zoomchange', + value: [scale.x, scale.y, scale.z], + }); + }; + + /** + * Add translation to the layers. + * + * @param {object} translation The translation as {x,y,z}. + */ + this.addTranslation = function (translation) { + this.setOffset({ + x: offset.x - translation.x, + y: offset.y - translation.y, + z: offset.z - translation.z + }); + }; + + /** + * Set the layers' offset. + * + * @param {object} newOffset The offset as {x,y,z}. + * @fires dwv.ctrl.LayerGroup#offsetchange + */ + this.setOffset = function (newOffset) { + // store + offset = newOffset; + // apply to layers + for (var i = 0; i < layers.length; ++i) { + layers[i].setOffset(offset); + } + + /** + * Offset change event. + * + * @event dwv.ctrl.LayerGroup#offsetchange + * @type {object} + * @property {Array} value The changed value. + */ + fireEvent({ + type: 'offsetchange', + value: [offset.x, offset.y, offset.z], + }); + }; + + /** + * Reset the stage to its initial scale and no offset. + */ + this.reset = function () { + this.setScale(baseScale); + this.setOffset({x: 0, y: 0, z: 0}); + }; + + /** + * Draw the layer. + */ + this.draw = function () { + for (var i = 0; i < layers.length; ++i) { + layers[i].draw(); + } + }; + + /** + * Display the layer. + * + * @param {boolean} flag Whether to display the layer or not. + */ + this.display = function (flag) { + for (var i = 0; i < layers.length; ++i) { + layers[i].display(flag); + } + }; + + /** + * Add an event listener to this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + + /** + * Remove an event listener from this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + + /** + * Fire an event: call all associated listeners with the input event object. + * + * @param {object} event The event to fire. + * @private + */ + function fireEvent(event) { + listenerHandler.fireEvent(event); + } + +}; // LayerGroup class diff --git a/src/gui/stage.js b/src/gui/stage.js new file mode 100644 index 0000000000..9409ae59ae --- /dev/null +++ b/src/gui/stage.js @@ -0,0 +1,346 @@ +// namespaces +var dwv = dwv || {}; +dwv.gui = dwv.gui || {}; + +/** + * Window/level binder. + */ +dwv.gui.WindowLevelBinder = function () { + this.getEventType = function () { + return 'wlchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + var viewLayers = layerGroup.getViewLayersByDataIndex(event.dataindex); + if (viewLayers.length !== 0) { + var vc = viewLayers[0].getViewController(); + vc.setWindowLevel(event.value[0], event.value[1]); + } + }; + }; +}; + +/** + * Position binder. + */ +dwv.gui.PositionBinder = function () { + this.getEventType = function () { + return 'positionchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + var pos = new dwv.math.Point(event.value[1]); + var vc = layerGroup.getActiveViewLayer().getViewController(); + vc.setCurrentPosition(pos); + }; + }; +}; + +/** + * Zoom binder. + */ +dwv.gui.ZoomBinder = function () { + this.getEventType = function () { + return 'zoomchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + layerGroup.setScale({ + x: event.value[0], + y: event.value[1], + z: event.value[2] + }); + layerGroup.draw(); + }; + }; +}; + +/** + * Offset binder. + */ +dwv.gui.OffsetBinder = function () { + this.getEventType = function () { + return 'offsetchange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + layerGroup.setOffset({ + x: event.value[0], + y: event.value[1], + z: event.value[2] + }); + layerGroup.draw(); + }; + }; +}; + +/** + * Opacity binder. Only propagates to view layers of the same data. + */ +dwv.gui.OpacityBinder = function () { + this.getEventType = function () { + return 'opacitychange'; + }; + this.getCallback = function (layerGroup) { + return function (event) { + // exit if no data index + if (typeof event.dataindex === 'undefined') { + return; + } + // propagate to first view layer + var viewLayers = layerGroup.getViewLayersByDataIndex(event.dataindex); + if (viewLayers.length !== 0) { + viewLayers[0].setOpacity(event.value); + viewLayers[0].draw(); + } + }; + }; +}; + +/** + * Stage: controls a list of layer groups and their + * synchronisation. + * + * @class + */ +dwv.gui.Stage = function () { + + // associated layer groups + var layerGroups = []; + // active layer group index + var activeLayerGroupIndex = null; + + // layer group binders + var binders = []; + // binder callbacks + var callbackStore = null; + + /** + * Get the layer group at the given index. + * + * @param {number} index The index. + * @returns {dwv.gui.LayerGroup} The layer group. + */ + this.getLayerGroup = function (index) { + return layerGroups[index]; + }; + + /** + * Get the number of layer groups that form the stage. + * + * @returns {number} The number of layer groups. + */ + this.getNumberOfLayerGroups = function () { + return layerGroups.length; + }; + + /** + * Get the active layer group. + * + * @returns {dwv.gui.LayerGroup} The layer group. + */ + this.getActiveLayerGroup = function () { + return this.getLayerGroup(activeLayerGroupIndex); + }; + + /** + * Get the view layers associated to a data index. + * + * @param {number} index The data index. + * @returns {Array} The layers. + */ + this.getViewLayersByDataIndex = function (index) { + var res = []; + for (var i = 0; i < layerGroups.length; ++i) { + res = res.concat(layerGroups[i].getViewLayersByDataIndex(index)); + } + return res; + }; + + /** + * Add a layer group to the list. + * + * @param {object} htmlElement The HTML element of the layer group. + * @returns {dwv.gui.LayerGroup} The newly created layer group. + */ + this.addLayerGroup = function (htmlElement) { + activeLayerGroupIndex = layerGroups.length; + var layerGroup = new dwv.gui.LayerGroup(htmlElement, activeLayerGroupIndex); + // add to storage + var isBound = callbackStore && callbackStore.length !== 0; + if (isBound) { + this.unbindLayerGroups(); + } + layerGroups.push(layerGroup); + if (isBound) { + this.bindLayerGroups(); + } + // return created group + return layerGroup; + }; + + /** + * Get a layer group from an HTML element id. + * + * @param {string} id The element id to find. + * @returns {dwv.gui.LayerGroup} The layer group. + */ + this.getLayerGroupWithElementId = function (id) { + return layerGroups.find(function (item) { + return item.getElementId() === id; + }); + }; + + /** + * Set the layer groups binders. + * + * @param {Array} list The list of binder objects. + */ + this.setBinders = function (list) { + if (typeof list === 'undefined' || list === null) { + throw new Error('Cannot set null or undefined binders'); + } + if (binders.length !== 0) { + this.unbindLayerGroups(); + } + binders = list.slice(); + this.bindLayerGroups(); + }; + + /** + * Empty the layer group list. + */ + this.empty = function () { + this.unbindLayerGroups(); + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].empty(); + } + layerGroups = []; + activeLayerGroupIndex = null; + }; + + /** + * Reset the stage: calls reset on all layer groups. + */ + this.reset = function () { + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].reset(); + } + }; + + /** + * Draw the stage: calls draw on all layer groups. + */ + this.draw = function () { + for (var i = 0; i < layerGroups.length; ++i) { + layerGroups[i].draw(); + } + }; + + /** + * Bind the layer groups of the stage. + */ + this.bindLayerGroups = function () { + if (layerGroups.length === 0 || + layerGroups.length === 1 || + binders.length === 0) { + return; + } + // create callback store + callbackStore = new Array(layerGroups.length); + // add listeners + for (var i = 0; i < layerGroups.length; ++i) { + for (var j = 0; j < binders.length; ++j) { + addEventListeners(i, binders[j]); + } + } + }; + + /** + * Unbind the layer groups of the stage. + */ + this.unbindLayerGroups = function () { + if (layerGroups.length === 0 || + layerGroups.length === 1 || + binders.length === 0 || + !callbackStore) { + return; + } + // remove listeners + for (var i = 0; i < layerGroups.length; ++i) { + for (var j = 0; j < binders.length; ++j) { + removeEventListeners(i, binders[j]); + } + } + // clear callback store + callbackStore = null; + }; + + /** + * Get the binder callback function for a given layer group index. + * The function is created if not yet stored. + * + * @param {object} binder The layer binder. + * @param {number} index The index of the associated layer group. + * @returns {Function} The binder function. + */ + function getBinderCallback(binder, index) { + if (typeof callbackStore[index] === 'undefined') { + callbackStore[index] = []; + } + var store = callbackStore[index]; + var binderObj = store.find(function (elem) { + return elem.binder === binder; + }); + if (typeof binderObj === 'undefined') { + // create new callback object + binderObj = { + binder: binder, + callback: function (event) { + // stop listeners + removeEventListeners(index, binder); + // apply binder + binder.getCallback(layerGroups[index])(event); + // re-start listeners + addEventListeners(index, binder); + } + }; + callbackStore[index].push(binderObj); + } + return binderObj.callback; + } + + /** + * Add event listeners for a given layer group index and binder. + * + * @param {number} index The index of the associated layer group. + * @param {object} binder The layer binder. + */ + function addEventListeners(index, binder) { + for (var i = 0; i < layerGroups.length; ++i) { + if (i !== index) { + layerGroups[index].addEventListener( + binder.getEventType(), + getBinderCallback(binder, i) + ); + } + } + } + + /** + * Remove event listeners for a given layer group index and binder. + * + * @param {number} index The index of the associated layer group. + * @param {object} binder The layer binder. + */ + function removeEventListeners(index, binder) { + for (var i = 0; i < layerGroups.length; ++i) { + if (i !== index) { + layerGroups[index].removeEventListener( + binder.getEventType(), + getBinderCallback(binder, i) + ); + } + } + } +}; diff --git a/src/gui/viewLayer.js b/src/gui/viewLayer.js index 92c17e39fb..f4bd8c5097 100644 --- a/src/gui/viewLayer.js +++ b/src/gui/viewLayer.js @@ -5,7 +5,8 @@ dwv.gui = dwv.gui || {}; /** * View layer. * - * @param {object} containerDiv The layer div. + * @param {object} containerDiv The layer div, its id will be used + * as this layer id. * @class */ dwv.gui.ViewLayer = function (containerDiv) { @@ -16,13 +17,6 @@ dwv.gui.ViewLayer = function (containerDiv) { // closure to self var self = this; - /** - * The image view. - * - * @private - * @type {object} - */ - var view = null; /** * The view controller. * @@ -32,19 +26,19 @@ dwv.gui.ViewLayer = function (containerDiv) { var viewController = null; /** - * The base canvas. + * The main display canvas. * * @private * @type {object} */ var canvas = null; /** - * A cache of the initial canvas. + * The offscreen canvas: used to store the raw, unscaled pixel data. * * @private * @type {object} */ - var cacheCanvas = null; + var offscreenCanvas = null; /** * The associated CanvasRenderingContext2D. * @@ -62,12 +56,20 @@ dwv.gui.ViewLayer = function (containerDiv) { var imageData = null; /** - * The layer size as {x,y}. + * The layer base size as {x,y}. * * @private * @type {object} */ - var layerSize; + var baseSize; + + /** + * The layer base spacing as {x,y}. + * + * @private + * @type {object} + */ + var baseSpacing; /** * The layer opacity. @@ -85,6 +87,14 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var scale = {x: 1, y: 1}; + /** + * The layer fit scale. + * + * @private + * @type {object} + */ + var fitScale = {x: 1, y: 1}; + /** * The layer offset. * @@ -93,6 +103,14 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var offset = {x: 0, y: 0}; + /** + * The base layer offset. + * + * @private + * @type {object} + */ + var baseOffset = {x: 0, y: 0}; + /** * Data update flag. * @@ -109,6 +127,15 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var dataIndex = null; + /** + * Get the associated data index. + * + * @returns {number} The index. + */ + this.getDataIndex = function () { + return dataIndex; + }; + /** * Listener handler. * @@ -117,6 +144,25 @@ dwv.gui.ViewLayer = function (containerDiv) { */ var listenerHandler = new dwv.utils.ListenerHandler(); + /** + * Set the associated view. + * + * @param {object} view The view. + */ + this.setView = function (view) { + // local listeners + view.addEventListener('wlchange', onWLChange); + view.addEventListener('colourchange', onColourChange); + view.addEventListener('positionchange', onPositionChange); + view.addEventListener('alphafuncchange', onAlphaFuncChange); + // view events + for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { + view.addEventListener(dwv.image.viewEventNames[j], fireEvent); + } + // create view controller + viewController = new dwv.ctrl.ViewController(view); + }; + /** * Get the view controller. * @@ -143,7 +189,7 @@ dwv.gui.ViewLayer = function (containerDiv) { this.onimagechange = function (event) { // event.value = [index, image] if (dataIndex === event.value[0]) { - view.setImage(event.value[1]); + viewController.setImage(event.value[1]); needsDataUpdate = true; } }; @@ -151,12 +197,33 @@ dwv.gui.ViewLayer = function (containerDiv) { // common layer methods [start] --------------- /** - * Get the layer size. + * Get the id of the layer. + * + * @returns {string} The string id. + */ + this.getId = function () { + return containerDiv.id; + }; + + /** + * Get the data full size, ie size * spacing. + * + * @returns {object} The full size as {x,y}. + */ + this.getFullSize = function () { + return { + x: baseSize.x * baseSpacing.x, + y: baseSize.y * baseSpacing.y + }; + }; + + /** + * Get the layer base size (without scale). * * @returns {object} The size as {x,y}. */ - this.getSize = function () { - return layerSize; + this.getBaseSize = function () { + return baseSize; }; /** @@ -175,6 +242,19 @@ dwv.gui.ViewLayer = function (containerDiv) { */ this.setOpacity = function (alpha) { opacity = Math.min(Math.max(alpha, 0), 1); + + /** + * Opacity change event. + * + * @event dwv.App#opacitychange + * @type {object} + * @property {string} type The event type. + */ + var event = { + type: 'opacitychange', + value: [opacity] + }; + fireEvent(event); }; /** @@ -183,38 +263,72 @@ dwv.gui.ViewLayer = function (containerDiv) { * @param {object} newScale The scale as {x,y}. */ this.setScale = function (newScale) { - scale = newScale; + var helper = viewController.getPlaneHelper(); + var orientedNewScale = helper.getOrientedXYZ(newScale); + scale = { + x: fitScale.x * orientedNewScale.x, + y: fitScale.y * orientedNewScale.y + }; }; /** - * Set the layer offset. + * Set the base layer offset. Resets the layer offset. * - * @param {object} newOffset The offset as {x,y}. + * @param {object} off The offset as {x,y}. */ - this.setOffset = function (newOffset) { - offset = newOffset; + this.setBaseOffset = function (off) { + var helper = viewController.getPlaneHelper(); + baseOffset = helper.getPlaneOffsetFromOffset3D({ + x: off.getX(), + y: off.getY(), + z: off.getZ() + }); + // reset offset + offset = baseOffset; }; /** - * Set the layer z-index. + * Set the layer offset. * - * @param {number} index The index. + * @param {object} newOffset The offset as {x,y}. */ - this.setZIndex = function (index) { - containerDiv.style.zIndex = index; + this.setOffset = function (newOffset) { + var helper = viewController.getPlaneHelper(); + var planeNewOffset = helper.getPlaneOffsetFromOffset3D(newOffset); + offset = { + x: baseOffset.x + planeNewOffset.x, + y: baseOffset.y + planeNewOffset.y + }; }; /** - * Resize the layer: update the window scale and layer sizes. + * Transform a display position to an index. * - * @param {object} newScale The layer scale as {x,y}. + * @param {number} x The X position. + * @param {number} y The Y position. + * @returns {dwv.math.Index} The equivalent index. */ - this.resize = function (newScale) { - // resize canvas - canvas.width = parseInt(layerSize.x * newScale.x, 10); - canvas.height = parseInt(layerSize.y * newScale.y, 10); - // set scale - this.setScale(newScale); + this.displayToPlaneIndex = function (x, y) { + var planePos = this.displayToPlanePos(x, y); + return new dwv.math.Index([ + Math.floor(planePos.x), + Math.floor(planePos.y) + ]); + }; + + this.displayToPlaneScale = function (x, y) { + return { + x: x / scale.x, + y: y / scale.y + }; + }; + + this.displayToPlanePos = function (x, y) { + var deScaled = this.displayToPlaneScale(x, y); + return { + x: deScaled.x + offset.x, + y: deScaled.y + offset.y + }; }; /** @@ -250,7 +364,10 @@ dwv.gui.ViewLayer = function (containerDiv) { * @type {object} * @property {string} type The event type. */ - var event = {type: 'renderstart'}; + var event = { + type: 'renderstart', + layerid: this.getId() + }; fireEvent(event); // update data if needed @@ -288,7 +405,7 @@ dwv.gui.ViewLayer = function (containerDiv) { // disable smoothing (set just before draw, could be reset by resize) context.imageSmoothingEnabled = false; // draw image - context.drawImage(cacheCanvas, 0, 0); + context.drawImage(offscreenCanvas, 0, 0); /** * Render end event. @@ -297,41 +414,25 @@ dwv.gui.ViewLayer = function (containerDiv) { * @type {object} * @property {string} type The event type. */ - event = {type: 'renderend'}; + event = { + type: 'renderend', + layerid: this.getId() + }; fireEvent(event); }; /** * Initialise the layer: set the canvas and context * - * @param {object} image The image. - * @param {object} metaData The image meta data. + * @param {object} size The image size as {x,y}. + * @param {object} spacing The image spacing as {x,y}. * @param {number} index The associated data index. */ - this.initialise = function (image, metaData, index) { + this.initialise = function (size, spacing, index) { + // set locals + baseSize = size; + baseSpacing = spacing; dataIndex = index; - // create view - var viewFactory = new dwv.image.ViewFactory(); - view = viewFactory.create( - new dwv.dicom.DicomElementsWrapper(metaData), - image); - - // local listeners - view.addEventListener('wlwidthchange', onWLChange); - view.addEventListener('wlcenterchange', onWLChange); - view.addEventListener('colourchange', onColourChange); - view.addEventListener('slicechange', onSliceChange); - view.addEventListener('framechange', onFrameChange); - - // create view controller - viewController = new dwv.ViewController(view); - - // get sizes - var size = image.getGeometry().getSize(); - layerSize = { - x: size.getNumberOfColumns(), - y: size.getNumberOfRows() - }; // create canvas canvas = document.createElement('canvas'); @@ -348,25 +449,55 @@ dwv.gui.ViewLayer = function (containerDiv) { alert('Error: failed to get the 2D context.'); return; } + + // check canvas + if (!dwv.gui.canCreateCanvas(baseSize.x, baseSize.y)) { + throw new Error('Cannot create canvas ' + baseSize.x + ', ' + baseSize.y); + } + // canvas sizes - canvas.width = layerSize.x; - canvas.height = layerSize.y; + canvas.width = baseSize.x; + canvas.height = baseSize.y; + // off screen canvas + offscreenCanvas = document.createElement('canvas'); + offscreenCanvas.width = baseSize.x; + offscreenCanvas.height = baseSize.y; // original empty image data array - context.clearRect(0, 0, canvas.width, canvas.height); - imageData = context.createImageData(canvas.width, canvas.height); - // cached canvas - cacheCanvas = document.createElement('canvas'); - cacheCanvas.width = canvas.width; - cacheCanvas.height = canvas.height; + context.clearRect(0, 0, baseSize.x, baseSize.y); + imageData = context.createImageData(baseSize.x, baseSize.y); // update data on first draw needsDataUpdate = true; }; /** - * Activate the layer: propagate events. + * Fit the layer to its parent container. + * + * @param {number} fitScale1D The 1D fit scale. + */ + this.fitToContainer = function (fitScale1D) { + // update fit scale + fitScale = { + x: fitScale1D * baseSpacing.x, + y: fitScale1D * baseSpacing.y + }; + // update canvas + var fullSize = this.getFullSize(); + var width = Math.floor(fullSize.x * fitScale1D); + var height = Math.floor(fullSize.y * fitScale1D); + if (!dwv.gui.canCreateCanvas(width, height)) { + throw new Error('Cannot resize canvas ' + width + ', ' + height); + } + canvas.width = width; + canvas.height = height; + // reset scale + this.setScale({x: 1, y: 1, z: 1}); + }; + + /** + * Enable and listen to container interaction events. */ - this.activate = function () { + this.bindInteraction = function () { // allow pointer events containerDiv.style.pointerEvents = 'auto'; // interaction events @@ -377,9 +508,9 @@ dwv.gui.ViewLayer = function (containerDiv) { }; /** - * Deactivate the layer: stop propagating events. + * Disable and stop listening to container interaction events. */ - this.deactivate = function () { + this.unbindInteraction = function () { // disable pointer events containerDiv.style.pointerEvents = 'none'; // interaction events @@ -418,35 +549,21 @@ dwv.gui.ViewLayer = function (containerDiv) { * @private */ function fireEvent(event) { + event.srclayerid = self.getId(); + event.dataindex = dataIndex; listenerHandler.fireEvent(event); } // common layer methods [end] --------------- - /** - * Propagate (or not) view events. - * - * @param {boolean} flag True to propagate. - */ - this.propagateViewEvents = function (flag) { - // view events - for (var j = 0; j < dwv.image.viewEventNames.length; ++j) { - if (flag) { - view.addEventListener(dwv.image.viewEventNames[j], fireEvent); - } else { - view.removeEventListener(dwv.image.viewEventNames[j], fireEvent); - } - } - }; - /** * Update the canvas image data. */ function updateImageData() { // generate image data - view.generateImageData(imageData); - // pass the data to the canvas - cacheCanvas.getContext('2d').putImageData(imageData, 0, 0); + viewController.generateImageData(imageData); + // pass the data to the off screen canvas + offscreenCanvas.getContext('2d').putImageData(imageData, 0, 0); // update data flag needsDataUpdate = false; } @@ -478,40 +595,53 @@ dwv.gui.ViewLayer = function (containerDiv) { } /** - * Handle frame change. + * Handle position change. * - * @param {object} event The event fired when changing the frame. + * @param {object} event The event fired when changing the position. * @private */ - function onFrameChange(event) { - // generate and draw if no skip flag + function onPositionChange(event) { if (typeof event.skipGenerate === 'undefined' || event.skipGenerate === false) { - needsDataUpdate = true; - self.draw(); + // 3D dimensions + var dims3D = [0, 1, 2]; + // remove scroll index + var indexScrollIndex = dims3D.indexOf(viewController.getScrollIndex()); + dims3D.splice(indexScrollIndex, 1); + // remove non scroll index from diff dims + var diffDims = event.diffDims.filter(function (item) { + return dims3D.indexOf(item) === -1; + }); + // update if we have something left + if (diffDims.length !== 0) { + needsDataUpdate = true; + self.draw(); + } } } /** - * Handle slice change. + * Handle alpha function change. * - * @param {object} _event The event fired when changing the slice. + * @param {object} event The event fired when changing the function. * @private */ - function onSliceChange(_event) { - needsDataUpdate = true; - self.draw(); + function onAlphaFuncChange(event) { + if (typeof event.skipGenerate === 'undefined' || + event.skipGenerate === false) { + needsDataUpdate = true; + self.draw(); + } } /** - * Update the layer position. + * Set the current position. * - * @param {object} pos The new position. + * @param {dwv.math.Point} position The new position. + * @param {dwv.math.Index} _index The new index. */ - this.updatePosition = function (pos) { - viewController.setCurrentPosition(pos[0]); - viewController.setCurrentFrame(pos[1]); - needsDataUpdate = true; + this.setCurrentPosition = function (position, _index) { + viewController.setCurrentPosition(position); }; /** diff --git a/src/image/decoder.js b/src/image/decoder.js index 2379d8f19c..7799ad0f24 100644 --- a/src/image/decoder.js +++ b/src/image/decoder.js @@ -40,27 +40,29 @@ var hasJpeg2000Decoder = (typeof JpxImage !== 'undefined'); */ dwv.image.AsynchPixelBufferDecoder = function (script, _numberOfData) { // initialise the thread pool - var pool = new dwv.utils.ThreadPool(15); + var pool = new dwv.utils.ThreadPool(10); // flag to know if callbacks are set var areCallbacksSet = false; + // closure to self + var self = this; /** * Decode a pixel buffer. * * @param {Array} pixelBuffer The pixel buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { if (!areCallbacksSet) { areCallbacksSet = true; // set event handlers - pool.onworkstart = this.ondecodestart; - pool.onworkitem = this.ondecodeditem; - pool.onwork = this.ondecoded; - pool.onworkend = this.ondecodeend; - pool.onerror = this.onerror; - pool.onabort = this.onabort; + pool.onworkstart = self.ondecodestart; + pool.onworkitem = self.ondecodeditem; + pool.onwork = self.ondecoded; + pool.onworkend = self.ondecodeend; + pool.onerror = self.onerror; + pool.onabort = self.onabort; } // create worker task var workerTask = new dwv.utils.WorkerTask( @@ -69,7 +71,7 @@ dwv.image.AsynchPixelBufferDecoder = function (script, _numberOfData) { buffer: pixelBuffer, meta: pixelMeta }, - index + info ); // add it the queue and run it pool.addWorkerTask(workerTask); @@ -150,12 +152,12 @@ dwv.image.SynchPixelBufferDecoder = function (algoName, numberOfData) { * * @param {Array} pixelBuffer The pixel buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. * @external jpeg * @external JpegImage * @external JpxImage */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { ++decodeCount; var decoder = null; @@ -213,7 +215,7 @@ dwv.image.SynchPixelBufferDecoder = function (algoName, numberOfData) { // send decode events this.ondecodeditem({ data: [decodedBuffer], - index: index + index: info.itemNumber }); // decode end? if (decodeCount === numberOfData) { @@ -304,7 +306,7 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { // initialise the asynch decoder (if possible) if (typeof dwv.image.decoderScripts !== 'undefined' && - typeof dwv.image.decoderScripts[algoName] !== 'undefined') { + typeof dwv.image.decoderScripts[algoName] !== 'undefined') { pixelDecoder = new dwv.image.AsynchPixelBufferDecoder( dwv.image.decoderScripts[algoName], numberOfData); } else { @@ -320,9 +322,9 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { * * @param {Array} pixelBuffer The input data buffer. * @param {object} pixelMeta The input meta data. - * @param {number} index The index of the input data. + * @param {object} info Information object about the input data. */ - this.decode = function (pixelBuffer, pixelMeta, index) { + this.decode = function (pixelBuffer, pixelMeta, info) { if (!areCallbacksSet) { areCallbacksSet = true; // set callbacks @@ -334,7 +336,7 @@ dwv.image.PixelBufferDecoder = function (algoName, numberOfData) { pixelDecoder.onabort = this.onabort; } // decode and call the callback - pixelDecoder.decode(pixelBuffer, pixelMeta, index); + pixelDecoder.decode(pixelBuffer, pixelMeta, info); }; /** diff --git a/src/image/dicomBufferToView.js b/src/image/dicomBufferToView.js index 3a15dbd1ea..d4b9c520db 100644 --- a/src/image/dicomBufferToView.js +++ b/src/image/dicomBufferToView.js @@ -12,20 +12,20 @@ dwv.image.DicomBufferToView = function () { var self = this; /** - * The default character set (optional). + * Converter options. * * @private - * @type {string} + * @type {object} */ - var defaultCharacterSet; + var options; /** - * Set the default character set. + * Set the converter options. * - * @param {string} characterSet The character set. + * @param {object} opt The input options. */ - this.setDefaultCharacterSet = function (characterSet) { - defaultCharacterSet = characterSet; + this.setOptions = function (opt) { + options = opt; }; /** @@ -37,6 +37,110 @@ dwv.image.DicomBufferToView = function () { */ var pixelDecoder = null; + // local tmp storage + var dicomParserStore = []; + var finalBufferStore = []; + var decompressedSizes = []; + + /** + * Generate the image object. + * + * @param {number} index The data index. + * @param {string} origin The data origin. + */ + function generateImage(index, origin) { + // create the image + try { + var imageFactory = new dwv.ImageFactory(); + var image = imageFactory.create( + dicomParserStore[index].getDicomElements(), + finalBufferStore[index], + options.numberOfFiles); + // call onloaditem + self.onloaditem({ + data: { + image: image, + info: dicomParserStore[index].getRawDicomElements() + }, + source: origin + }); + } catch (error) { + self.onerror({ + error: error, + source: origin + }); + self.onloadend({ + source: origin + }); + } + } + + /** + * Handle a decoded item event. + * + * @param {object} event The decoded item event. + */ + function onDecodedItem(event) { + // send progress + self.onprogress({ + lengthComputable: true, + loaded: event.itemNumber + 1, + total: event.numberOfItems, + index: event.dataIndex, + source: origin + }); + + var dataIndex = event.dataIndex; + + // store decoded data + var decodedData = event.data[0]; + if (event.numberOfItems !== 1) { + // allocate buffer if not done yet + if (typeof decompressedSizes[dataIndex] === 'undefined') { + decompressedSizes[dataIndex] = decodedData.length; + var fullSize = event.numberOfItems * decompressedSizes[dataIndex]; + try { + finalBufferStore[dataIndex] = new decodedData.constructor(fullSize); + } catch (error) { + if (error instanceof RangeError) { + var powerOf2 = Math.floor(Math.log(fullSize) / Math.log(2)); + dwv.logger.error('Cannot allocate ' + + decodedData.constructor.name + + ' of size: ' + + fullSize + ' (>2^' + powerOf2 + ') for decompressed data.'); + } + // abort + pixelDecoder.abort(); + // send events + self.onerror({ + error: error, + source: origin + }); + self.onloadend({ + source: origin + }); + // exit + return; + } + } + // hoping for all items to have the same size... + if (decodedData.length !== decompressedSizes[dataIndex]) { + dwv.logger.warn('Unsupported varying decompressed data size: ' + + decodedData.length + ' != ' + decompressedSizes[dataIndex]); + } + // set buffer item data + finalBufferStore[dataIndex].set( + decodedData, decompressedSizes[dataIndex] * event.itemNumber); + } else { + finalBufferStore[dataIndex] = decodedData; + } + + // create image for the first item + if (event.itemNumber === 0) { + generateImage(dataIndex, origin); + } + } + /** * Get data from an input buffer using a DICOM parser. * @@ -46,15 +150,22 @@ dwv.image.DicomBufferToView = function () { */ this.convert = function (buffer, origin, dataIndex) { self.onloadstart({ - source: origin + source: origin, + dataIndex: dataIndex }); // DICOM parser var dicomParser = new dwv.dicom.DicomParser(); - dicomParser.setDefaultCharacterSet(defaultCharacterSet); + var imageFactory = new dwv.ImageFactory(); + + if (typeof options.defaultCharacterSet !== 'undefined') { + dicomParser.setDefaultCharacterSet(options.defaultCharacterSet); + } // parse the buffer try { dicomParser.parse(buffer); + // check elements are good for image + imageFactory.checkElements(dicomParser.getDicomElements()); } catch (error) { self.onerror({ error: error, @@ -67,36 +178,16 @@ dwv.image.DicomBufferToView = function () { } var pixelBuffer = dicomParser.getRawDicomElements().x7FE00010.value; + // help GC: discard pixel buffer from elements + dicomParser.getRawDicomElements().x7FE00010.value = []; var syntax = dwv.dicom.cleanString( dicomParser.getRawDicomElements().x00020010.value[0]); var algoName = dwv.dicom.getSyntaxDecompressionName(syntax); var needDecompression = (algoName !== null); - // generate the image - var generateImage = function (/*event*/) { - // create the image - var imageFactory = new dwv.image.ImageFactory(); - try { - var image = imageFactory.create( - dicomParser.getDicomElements(), pixelBuffer); - // call onload - self.onloaditem({ - data: { - image: image, - info: dicomParser.getRawDicomElements() - }, - source: origin - }); - } catch (error) { - self.onerror({ - error: error, - source: origin - }); - self.onloadend({ - source: origin - }); - } - }; + // store + dicomParserStore[dataIndex] = dicomParser; + finalBufferStore[dataIndex] = pixelBuffer[0]; if (needDecompression) { // gather pixel buffer meta data @@ -123,45 +214,38 @@ dwv.image.DicomBufferToView = function () { pixelMeta.planarConfiguration = planarConfigurationElement.value[0]; } - // number of frames - var numberOfFrames = pixelBuffer.length; - - // decoder callback - var countDecodedFrames = 0; - var onDecodedFrame = function (event) { - ++countDecodedFrames; - // send progress - self.onprogress({ - lengthComputable: true, - loaded: (countDecodedFrames * 100 / numberOfFrames), - total: 100, - index: dataIndex, - source: origin - }); - // store data - var frameNb = event.index; - pixelBuffer[frameNb] = event.data[0]; - // create image for the first frame - if (frameNb === 0) { - generateImage(); - } - }; + // number of items + var numberOfItems = pixelBuffer.length; - // setup the decoder (one decoder per convert) - // TODO check if it is ok to create a worker pool per file... - pixelDecoder = new dwv.image.PixelBufferDecoder( - algoName, numberOfFrames); - // callbacks - // pixelDecoder.ondecodestart: nothing to do - pixelDecoder.ondecodeditem = onDecodedFrame; - pixelDecoder.ondecoded = self.onload; - pixelDecoder.ondecodeend = self.onloadend; - pixelDecoder.onerror = self.onerror; - pixelDecoder.onabort = self.onabort; + // setup the decoder (one decoder per all converts) + if (pixelDecoder === null) { + pixelDecoder = new dwv.image.PixelBufferDecoder( + algoName, numberOfItems); + // callbacks + // pixelDecoder.ondecodestart: nothing to do + pixelDecoder.ondecodeditem = function (event) { + onDecodedItem(event); + // send onload and onloadend when all items have been decoded + if (event.itemNumber + 1 === event.numberOfItems) { + self.onload(event); + self.onloadend(event); + } + }; + // pixelDecoder.ondecoded: nothing to do + // pixelDecoder.ondecodeend: nothing to do + pixelDecoder.onerror = self.onerror; + pixelDecoder.onabort = self.onabort; + } // launch decode - for (var f = 0; f < numberOfFrames; ++f) { - pixelDecoder.decode(pixelBuffer[f], pixelMeta, f); + for (var i = 0; i < numberOfItems; ++i) { + pixelDecoder.decode(pixelBuffer[i], pixelMeta, + { + itemNumber: i, + numberOfItems: numberOfItems, + dataIndex: dataIndex + } + ); } } else { // no decompression @@ -174,7 +258,7 @@ dwv.image.DicomBufferToView = function () { source: origin }); // generate image - generateImage(); + generateImage(dataIndex, origin); // send load events self.onload({ source: origin @@ -203,6 +287,13 @@ dwv.image.DicomBufferToView = function () { * @param {object} _event The load start event. */ dwv.image.DicomBufferToView.prototype.onloadstart = function (_event) {}; +/** + * Handle a load item event. + * Default does nothing. + * + * @param {object} _event The load item event. + */ +dwv.image.DicomBufferToView.prototype.onloaditem = function (_event) {}; /** * Handle a load progress event. * Default does nothing. diff --git a/src/image/domReader.js b/src/image/domReader.js index 3612bdbf82..9221c6286c 100644 --- a/src/image/domReader.js +++ b/src/image/domReader.js @@ -39,20 +39,22 @@ dwv.image.getDefaultImage = function ( imageBuffer, numberOfFrames, imageUid) { // image size - var imageSize = new dwv.image.Size(width, height); + var imageSize = new dwv.image.Size([width, height, 1]); // default spacing // TODO: misleading... - var imageSpacing = new dwv.image.Spacing(1, 1); + var imageSpacing = new dwv.image.Spacing([1, 1, 1]); // default origin var origin = new dwv.math.Point3D(0, 0, sliceIndex); // create image var geometry = new dwv.image.Geometry(origin, imageSize, imageSpacing); - var image = new dwv.image.Image( - geometry, imageBuffer, numberOfFrames, [imageUid]); + var image = new dwv.image.Image(geometry, imageBuffer, [imageUid]); image.setPhotometricInterpretation('RGB'); // meta information var meta = {}; meta.BitsStored = 8; + if (typeof numberOfFrames !== 'undefined') { + meta.numberOfFiles = numberOfFrames; + } image.setMeta(meta); // return return image; @@ -97,7 +99,7 @@ dwv.image.getViewFromDOMImage = function (domImage, origin) { // create view var imageBuffer = dwv.image.imageDataToBuffer(imageData); var image = dwv.image.getDefaultImage( - width, height, sliceIndex, [imageBuffer], 1, sliceIndex); + width, height, sliceIndex, imageBuffer, 1, sliceIndex); // return return { @@ -130,7 +132,7 @@ dwv.image.getViewFromDOMVideo = function ( // default frame rate... var frameRate = 30; // number of frames - var numberOfFrames = Math.floor(video.duration * frameRate); + var numberOfFrames = Math.ceil(video.duration * frameRate); // video properties var info = {}; @@ -142,6 +144,7 @@ dwv.image.getViewFromDOMVideo = function ( info['imageWidth'] = {value: width}; info['imageHeight'] = {value: height}; info['numberOfFrames'] = {value: numberOfFrames}; + info['imageUid'] = {value: 0}; // draw the image in the canvas in order to get its data var canvas = document.createElement('canvas'); @@ -177,7 +180,7 @@ dwv.image.getViewFromDOMVideo = function ( if (frameIndex === 0) { // create view image = dwv.image.getDefaultImage( - width, height, 1, [imgBuffer], numberOfFrames, dataIndex); + width, height, 1, imgBuffer, numberOfFrames, dataIndex); // call callback onloaditem({ data: { @@ -187,7 +190,7 @@ dwv.image.getViewFromDOMVideo = function ( source: origin }); } else { - image.appendFrameBuffer(imgBuffer); + image.appendFrameBuffer(imgBuffer, frameIndex); } // increment index ++frameIndex; diff --git a/src/image/filter.js b/src/image/filter.js index 14aa2dff21..2409671f44 100644 --- a/src/image/filter.js +++ b/src/image/filter.js @@ -70,13 +70,13 @@ dwv.image.filter.Threshold = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -84,7 +84,7 @@ dwv.image.filter.Threshold = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -94,7 +94,7 @@ dwv.image.filter.Threshold = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Threshold.prototype.update = function () { var image = this.getOriginalImage(); @@ -128,13 +128,13 @@ dwv.image.filter.Sharpen = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -142,7 +142,7 @@ dwv.image.filter.Sharpen = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -152,7 +152,7 @@ dwv.image.filter.Sharpen = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Sharpen.prototype.update = function () { var image = this.getOriginalImage(); @@ -187,13 +187,13 @@ dwv.image.filter.Sobel = function () { * Original image. * * @private - * @type {object} + * @type {dwv.image.Image} */ var originalImage = null; /** * Set the original image. * - * @param {object} image The original image. + * @param {dwv.image.Image} image The original image. */ this.setOriginalImage = function (image) { originalImage = image; @@ -201,7 +201,7 @@ dwv.image.filter.Sobel = function () { /** * Get the original image. * - * @returns {object} image The original image. + * @returns {dwv.image.Image} image The original image. */ this.getOriginalImage = function () { return originalImage; @@ -211,7 +211,7 @@ dwv.image.filter.Sobel = function () { /** * Transform the main image using this filter. * - * @returns {object} The transformed image. + * @returns {dwv.image.Image} The transformed image. */ dwv.image.filter.Sobel.prototype.update = function () { var image = this.getOriginalImage(); diff --git a/src/image/geometry.js b/src/image/geometry.js index 454aec01d9..bfd171a884 100644 --- a/src/image/geometry.js +++ b/src/image/geometry.js @@ -2,168 +2,14 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; -/** - * 2D/3D Size class. - * - * @class - * @param {number} numberOfColumns The number of columns. - * @param {number} numberOfRows The number of rows. - * @param {number} numberOfSlices The number of slices. - */ -dwv.image.Size = function (numberOfColumns, numberOfRows, numberOfSlices) { - /** - * Get the number of columns. - * - * @returns {number} The number of columns. - */ - this.getNumberOfColumns = function () { - return numberOfColumns; - }; - /** - * Get the number of rows. - * - * @returns {number} The number of rows. - */ - this.getNumberOfRows = function () { - return numberOfRows; - }; - /** - * Get the number of slices. - * - * @returns {number} The number of slices. - */ - this.getNumberOfSlices = function () { - return (numberOfSlices || 1.0); - }; -}; - -/** - * Get the size of a slice. - * - * @returns {number} The size of a slice. - */ -dwv.image.Size.prototype.getSliceSize = function () { - return this.getNumberOfColumns() * this.getNumberOfRows(); -}; - -/** - * Get the total size. - * - * @returns {number} The total size. - */ -dwv.image.Size.prototype.getTotalSize = function () { - return this.getSliceSize() * this.getNumberOfSlices(); -}; - -/** - * Check for equality. - * - * @param {dwv.image.Size} rhs The object to compare to. - * @returns {boolean} True if both objects are equal. - */ -dwv.image.Size.prototype.equals = function (rhs) { - return rhs !== null && - this.getNumberOfColumns() === rhs.getNumberOfColumns() && - this.getNumberOfRows() === rhs.getNumberOfRows() && - this.getNumberOfSlices() === rhs.getNumberOfSlices(); -}; - -/** - * Check that coordinates are within bounds. - * - * @param {number} i The column coordinate. - * @param {number} j The row coordinate. - * @param {number} k The slice coordinate. - * @returns {boolean} True if the given coordinates are within bounds. - */ -dwv.image.Size.prototype.isInBounds = function (i, j, k) { - if (i < 0 || i > this.getNumberOfColumns() - 1 || - j < 0 || j > this.getNumberOfRows() - 1 || - k < 0 || k > this.getNumberOfSlices() - 1) { - return false; - } - return true; -}; - -/** - * Get a string representation of the Vector3D. - * - * @returns {string} The vector as a string. - */ -dwv.image.Size.prototype.toString = function () { - return '(' + this.getNumberOfColumns() + - ', ' + this.getNumberOfRows() + - ', ' + this.getNumberOfSlices() + ')'; -}; - -/** - * 2D/3D Spacing class. - * - * @class - * @param {number} columnSpacing The column spacing. - * @param {number} rowSpacing The row spacing. - * @param {number} sliceSpacing The slice spacing. - */ -dwv.image.Spacing = function (columnSpacing, rowSpacing, sliceSpacing) { - /** - * Get the column spacing. - * - * @returns {number} The column spacing. - */ - this.getColumnSpacing = function () { - return columnSpacing; - }; - /** - * Get the row spacing. - * - * @returns {number} The row spacing. - */ - this.getRowSpacing = function () { - return rowSpacing; - }; - /** - * Get the slice spacing. - * - * @returns {number} The slice spacing. - */ - this.getSliceSpacing = function () { - return (sliceSpacing || 1.0); - }; -}; - -/** - * Check for equality. - * - * @param {dwv.image.Spacing} rhs The object to compare to. - * @returns {boolean} True if both objects are equal. - */ -dwv.image.Spacing.prototype.equals = function (rhs) { - return rhs !== null && - this.getColumnSpacing() === rhs.getColumnSpacing() && - this.getRowSpacing() === rhs.getRowSpacing() && - this.getSliceSpacing() === rhs.getSliceSpacing(); -}; - -/** - * Get a string representation of the Vector3D. - * - * @returns {string} The vector as a string. - */ -dwv.image.Spacing.prototype.toString = function () { - return '(' + this.getColumnSpacing() + - ', ' + this.getRowSpacing() + - ', ' + this.getSliceSpacing() + ')'; -}; - - /** * 2D/3D Geometry class. * * @class - * @param {object} origin The object origin (a 3D point). - * @param {object} size The object size. - * @param {object} spacing The object spacing. - * @param {object} orientation The object orientation (3*3 matrix, + * @param {dwv.math.Point3D} origin The object origin (a 3D point). + * @param {dwv.image.Size} size The object size. + * @param {dwv.image.Spacing} spacing The object spacing. + * @param {dwv.math.Matrix33} orientation The object orientation (3*3 matrix, * default to 3*3 identity). */ dwv.image.Geometry = function (origin, size, spacing, orientation) { @@ -176,14 +22,17 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { if (typeof orientation === 'undefined') { orientation = new dwv.math.getIdentityMat33(); } + // flag to know if new origins were added + var newOrigins = false; /** - * Get the object first origin. + * Get the object origin. + * This should be the lowest origin to ease calculations (?). * - * @returns {object} The object first origin. + * @returns {dwv.math.Point3D} The object origin. */ this.getOrigin = function () { - return origin; + return origins[origins.length - 1]; }; /** * Get the object origins. @@ -195,24 +44,108 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { }; /** * Get the object size. + * Warning: the size comes as stored in DICOM, meaning that it could + * be oriented. * - * @returns {object} The object size. + * @param {dwv.math.Matrix33} viewOrientation The view orientation (optional) + * @returns {dwv.image.Size} The object size. */ - this.getSize = function () { - return size; + this.getSize = function (viewOrientation) { + var res = size; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var values = dwv.math.getOrientedArray3D( + [ + size.get(0), + size.get(1), + size.get(2) + ], + viewOrientation); + res = new dwv.image.Size(values); + } + return res; }; + /** - * Get the object spacing. + * Get the slice spacing from the difference in the Z directions + * of the origins. * - * @returns {object} The object spacing. + * @returns {number} The spacing. */ - this.getSpacing = function () { + this.getSliceGeometrySpacing = function () { + if (origins.length === 1) { + return 1; + } + var spacing = null; + // (x, y, z) = orientationMatrix * (i, j, k) + // -> inv(orientationMatrix) * (x, y, z) = (i, j, k) + // applied on the patient position, reorders indices + // so that Z is the slice direction + var orientation2 = orientation.getInverse().asOneAndZeros(); + var deltas = []; + for (var i = 0; i < origins.length - 1; ++i) { + var origin1 = orientation2.multiplyVector3D(origins[i]); + var origin2 = orientation2.multiplyVector3D(origins[i + 1]); + var diff = Math.abs(origin1.getZ() - origin2.getZ()); + if (diff === 0) { + throw new Error('Zero slice spacing.' + + origin1.toString() + ' ' + origin2.toString()); + } + if (spacing === null) { + spacing = diff; + } else { + if (!dwv.math.isSimilar(spacing, diff, dwv.math.BIG_EPSILON)) { + deltas.push(Math.abs(spacing - diff)); + } + } + } + // warn if non constant + if (deltas.length !== 0) { + var sumReducer = function (sum, value) { + return sum + value; + }; + var mean = deltas.reduce(sumReducer) / deltas.length; + if (mean > 1e-4) { + dwv.logger.warn('Varying slice spacing, mean delta: ' + + mean.toFixed(3) + ' (' + deltas.length + ' case(s))'); + } + } return spacing; }; + + /** + * Get the object spacing. + * Warning: the size comes as stored in DICOM, meaning that it could + * be oriented. + * + * @param {dwv.math.Matrix33} viewOrientation The view orientation (optional) + * @returns {dwv.image.Spacing} The object spacing. + */ + this.getSpacing = function (viewOrientation) { + // update slice spacing after appendSlice + if (newOrigins) { + var values = spacing.getValues(); + values[2] = this.getSliceGeometrySpacing(); + spacing = new dwv.image.Spacing(values); + newOrigins = false; + } + var res = spacing; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var orientedValues = dwv.math.getOrientedArray3D( + [ + spacing.get(0), + spacing.get(1), + spacing.get(2) + ], + viewOrientation); + res = new dwv.image.Spacing(orientedValues); + } + return res; + }; + /** * Get the object orientation. * - * @returns {object} The object orientation. + * @returns {dwv.math.Matrix33} The object orientation. */ this.getOrientation = function () { return orientation; @@ -220,8 +153,14 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { /** * Get the slice position of a point in the current slice layout. + * Slice indices increase with decreasing origins (high index -> low origin), + * this simplified the handling of reconstruction since it means + * the displayed data is in the same 'direction' as the extracted data. + * As seen in the getOrigin method, the main origin is the lowest one. + * This implies that the index to world and reverse method do some flipping + * magic... * - * @param {object} point The point to evaluate. + * @param {dwv.math.Point3D} point The point to evaluate. * @returns {number} The slice index. */ this.getSliceIndex = function (point) { @@ -239,36 +178,64 @@ dwv.image.Geometry = function (origin, size, spacing, orientation) { closestSliceIndex = i; } } - // we have the closest point, are we before or after + var closestOrigin = origins[closestSliceIndex]; + // direction between the input point and the closest origin + var pointDir = point.minus(closestOrigin); + // use third orientation matrix column as base plane vector var normal = new dwv.math.Vector3D( orientation.get(2, 0), orientation.get(2, 1), orientation.get(2, 2)); - var dotProd = normal.dotProduct(point.minus(origins[closestSliceIndex])); - var sliceIndex = (dotProd > 0) ? closestSliceIndex + 1 : closestSliceIndex; + // a.dot(b) = ||a|| * ||b|| * cos(theta) + // (https://en.wikipedia.org/wiki/Dot_product#Geometric_definition) + // -> the sign of the dot product depends on the cosinus of + // the angle between the vectors + // -> >0 => vectors are codirectional + // -> <0 => vectors are oposite + var dotProd = normal.dotProduct(pointDir); + // oposite vectors get higher index + var sliceIndex = (dotProd < 0) ? closestSliceIndex + 1 : closestSliceIndex; return sliceIndex; }; /** * Append an origin to the geometry. * - * @param {object} origin The origin to append. + * @param {dwv.math.Point3D} origin The origin to append. * @param {number} index The index at which to append. */ this.appendOrigin = function (origin, index) { + newOrigins = true; // add in origin array origins.splice(index, 0, origin); - // increment slice number - size = new dwv.image.Size( - size.getNumberOfColumns(), - size.getNumberOfRows(), - size.getNumberOfSlices() + 1); + // increment second dimension + var values = size.getValues(); + values[2] += 1; + size = new dwv.image.Size(values); + }; + + /** + * Append a frame to the geometry. + * + */ + this.appendFrame = function () { + // increment third dimension + var sizeValues = size.getValues(); + var spacingValues = spacing.getValues(); + if (sizeValues.length === 4) { + sizeValues[3] += 1; + } else { + sizeValues.push(2); + spacingValues.push(1); + } + size = new dwv.image.Size(sizeValues); + spacing = new dwv.image.Spacing(spacingValues); }; }; /** - * Get a string representation of the Vector3D. + * Get a string representation of the geometry. * - * @returns {string} The vector as a string. + * @returns {string} The geometry as a string. */ dwv.image.Geometry.prototype.toString = function () { return 'Origin: ' + this.getOrigin() + @@ -290,44 +257,113 @@ dwv.image.Geometry.prototype.equals = function (rhs) { }; /** - * Convert an index to an offset in memory. + * Check that a point is within bounds. * - * @param {object} index The index to convert. - * @returns {number} The offset + * @param {dwv.math.Point} point The point to check. + * @returns {boolean} True if the given coordinates are within bounds. */ -dwv.image.Geometry.prototype.indexToOffset = function (index) { - var size = this.getSize(); - return index.getI() + - index.getJ() * size.getNumberOfColumns() + - index.getK() * size.getSliceSize(); +dwv.image.Geometry.prototype.isInBounds = function (point) { + // get the corresponding index + var index = this.worldToIndex(point); + return this.getSize().isInBounds(index); }; +/** + * Flip the K index. + * + * @param {dwv.image.Size} size The image size. + * @param {number} k The index. + * @returns {number} The flipped index. + */ +function flipK(size, k) { + return (size.get(2) - 1) - k; +} + /** * Convert an index into world coordinates. * - * @param {object} index The index to convert. - * @returns {dwv.image.Point3D} The corresponding point. + * @param {dwv.math.Index} index The index to convert. + * @returns {dwv.math.Point} The corresponding point. */ dwv.image.Geometry.prototype.indexToWorld = function (index) { + // flip K index (because of the slice order given by getSliceIndex) + var k = flipK(this.getSize(), index.get(2)); + // apply spacing + // (spacing is oriented, apply before orientation) + var spacing = this.getSpacing(); + var orientedPoint3D = new dwv.math.Point3D( + index.get(0) * spacing.get(0), + index.get(1) * spacing.get(1), + k * spacing.get(2) + ); + // de-orient + var point3D = this.getOrientation().multiplyPoint3D(orientedPoint3D); + // keep >3d values + var values = index.getValues(); var origin = this.getOrigin(); + values[0] = origin.getX() + point3D.getX(); + values[1] = origin.getY() + point3D.getY(); + values[2] = origin.getZ() + point3D.getZ(); + // return point + return new dwv.math.Point(values); +}; + +/** + * Convert a 3D point into world coordinates. + * + * @param {dwv.math.Point3D} point The 3D point to convert. + * @returns {dwv.math.Point3D} The corresponding world 3D point. + */ +dwv.image.Geometry.prototype.pointToWorld = function (point) { + // flip K index (because of the slice order given by getSliceIndex) + var k = flipK(this.getSize(), point.getZ()); + // apply spacing + // (spacing is oriented, apply before orientation) var spacing = this.getSpacing(); + var orientedPoint3D = new dwv.math.Point3D( + point.getX() * spacing.get(0), + point.getY() * spacing.get(1), + k * spacing.get(2) + ); + // de-orient + var point3D = this.getOrientation().multiplyPoint3D(orientedPoint3D); + // return point3D + var origin = this.getOrigin(); return new dwv.math.Point3D( - origin.getX() + index.getI() * spacing.getColumnSpacing(), - origin.getY() + index.getJ() * spacing.getRowSpacing(), - origin.getZ() + index.getK() * spacing.getSliceSpacing()); + origin.getX() + point3D.getX(), + origin.getY() + point3D.getY(), + origin.getZ() + point3D.getZ() + ); }; /** * Convert world coordinates into an index. * - * @param {object} point The point to convert. - * @returns {dwv.image.Index} The corresponding index. + * @param {dwv.math.Point} point The point to convert. + * @returns {dwv.math.Index} The corresponding index. */ dwv.image.Geometry.prototype.worldToIndex = function (point) { + // compensate for origin + // (origin is not oriented, compensate before orientation) var origin = this.getOrigin(); + var point3D = new dwv.math.Point3D( + point.get(0) - origin.getX(), + point.get(1) - origin.getY(), + point.get(2) - origin.getZ() + ); + // orient + var orientedPoint3D = + this.getOrientation().getInverse().multiplyPoint3D(point3D); + // keep >3d values + var values = point.getValues(); + // apply spacing and round var spacing = this.getSpacing(); - return new dwv.math.Point3D( - point.getX() / spacing.getColumnSpacing() - origin.getX(), - point.getY() / spacing.getRowSpacing() - origin.getY(), - point.getZ() / spacing.getSliceSpacing() - origin.getZ()); + values[0] = Math.round(orientedPoint3D.getX() / spacing.get(0)); + values[1] = Math.round(orientedPoint3D.getY() / spacing.get(1)); + // flip K index (because of the slice order given by getSliceIndex) + values[2] = flipK(this.getSize(), + Math.round(orientedPoint3D.getZ() / spacing.get(2)) + ); + // return index + return new dwv.math.Index(values); }; diff --git a/src/image/image.js b/src/image/image.js index c4cb164738..f8b05b43be 100644 --- a/src/image/image.js +++ b/src/image/image.js @@ -11,39 +11,26 @@ dwv.image = dwv.image || {}; * - planar configuration (default RGBRGB...). * * @class - * @param {object} geometry The geometry of the image. - * @param {Array} buffer The image data as an array of frame buffers. - * @param {number} numberOfFrames The number of frames (optional, can be used - to anticipate the final number after appends). + * @param {dwv.image.Geometry} geometry The geometry of the image. + * @param {Array} buffer The image data as a one dimensional buffer. * @param {Array} imageUids An array of Uids indexed to slice number. */ -dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { - // use buffer length in not specified - if (typeof numberOfFrames === 'undefined') { - numberOfFrames = buffer.length; - } +dwv.image.Image = function (geometry, buffer, imageUids) { /** - * Get the number of frames. + * Constant rescale slope and intercept (default). * - * @returns {number} The number of frames. + * @private + * @type {object} */ - this.getNumberOfFrames = function () { - return numberOfFrames; - }; - + var rsi = new dwv.image.RescaleSlopeAndIntercept(1, 0); /** - * Rescale slope and intercept. + * Varying rescale slope and intercept. * * @private - * @type {number} + * @type {Array} */ - var rsis = []; - // initialise RSIs - for (var s = 0, nslices = geometry.getSize().getNumberOfSlices(); - s < nslices; ++s) { - rsis.push(new dwv.image.RescaleSlopeAndIntercept(1, 0)); - } + var rsis = null; /** * Flag to know if the RSIs are all identity (1,0). * @@ -73,31 +60,20 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { * @type {number} */ var planarConfiguration = 0; - - /** - * Check if the input element is not null. - * - * @param {object} element The element to test. - * @returns {boolean} True if the input is not null. - */ - var isNotNull = function (element) { - return element !== null; - }; - /** * Number of components. * * @private * @type {number} */ - var numberOfComponents = buffer.find(isNotNull).length / ( + var numberOfComponents = buffer.length / ( geometry.getSize().getTotalSize()); - /** - * Meta information. - * - * @private - * @type {object} - */ + /** + * Meta information. + * + * @private + * @type {object} + */ var meta = {}; /** @@ -123,18 +99,31 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { var histogram = null; /** - * Get the image UIDs indexed by slice number. + * Listener handler. + * + * @private + * @type {object} + */ + var listenerHandler = new dwv.utils.ListenerHandler(); + + /** + * Get the image UID at a given index. * - * @returns {Array} The UIDs array. + * @param {dwv.math.Index} index The index at which to get the id. + * @returns {string} The UID. */ - this.getImageUids = function () { - return imageUids; + this.getImageUid = function (index) { + var uid = imageUids[0]; + if (imageUids.length !== 1 && typeof index !== 'undefined') { + uid = imageUids[this.getSecondaryOffset(index)]; + } + return uid; }; /** * Get the geometry of the image. * - * @returns {object} The size of the image. + * @returns {dwv.image.Geometry} The geometry. */ this.getGeometry = function () { return geometry; @@ -149,47 +138,129 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { this.getBuffer = function () { return buffer; }; + /** - * Get the data buffer of the image. + * Can the image values be quantified? * - * @param {number} frame The frame number. - * @todo dangerous... - * @returns {Array} The data buffer of the frame. + * @returns {boolean} True if only one component. + */ + this.canQuantify = function () { + return this.getNumberOfComponents() === 1; + }; + + /** + * Can window and level be applied to the data? + * + * @returns {boolean} True if the data is monochrome. + */ + this.canWindowLevel = function () { + return this.getPhotometricInterpretation() + .match(/MONOCHROME/) !== null; + }; + + /** + * Can the data be scrolled? + * + * @param {dwv.math.Matrix33} viewOrientation The view orientation. + * @returns {boolean} True if the data has a third dimension greater than one + * after applying the view orientation. + */ + this.canScroll = function (viewOrientation) { + var size = this.getGeometry().getSize(); + // also check the numberOfFiles in case we are in the middle of a load + var nFiles = 1; + if (typeof meta.numberOfFiles !== 'undefined') { + nFiles = meta.numberOfFiles; + } + return size.canScroll(viewOrientation) || nFiles !== 1; + }; + + /** + * Get the secondary offset max. + * + * @returns {number} The maximum offset. */ - this.getFrame = function (frame) { - return buffer[frame]; + function getSecondaryOffsetMax() { + return geometry.getSize().getTotalSize(2); + } + + /** + * Get the secondary offset: an offset that takes into account + * the slice and above dimension numbers. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The offset. + */ + this.getSecondaryOffset = function (index) { + return geometry.getSize().indexToOffset(index, 2); }; /** * Get the rescale slope and intercept. * - * @param {number} k The slice index. + * @param {dwv.math.Index} index The index (only needed for non constant rsi). * @returns {object} The rescale slope and intercept. */ - this.getRescaleSlopeAndIntercept = function (k) { - return rsis[k]; + this.getRescaleSlopeAndIntercept = function (index) { + var res = rsi; + if (!this.isConstantRSI()) { + if (typeof index === 'undefined') { + throw new Error('Cannot get non constant RSI with empty slice index.'); + } + var offset = this.getSecondaryOffset(index); + if (typeof rsis[offset] !== 'undefined') { + res = rsis[offset]; + } else { + dwv.logger.warn('undefined non constant rsi at ' + offset); + } + } + return res; }; + /** - * Set the rescale slope and intercept. + * Get the rsi at a specified (secondary) offset. * - * @param {Array} inRsi The input rescale slope and intercept. - * @param {number} k The slice index (optional). + * @param {number} offset The desired (secondary) offset. + * @returns {object} The coresponding rsi. */ - this.setRescaleSlopeAndIntercept = function (inRsi, k) { - if (typeof k === 'undefined') { - k = 0; - } - rsis[k] = inRsi; + function getRescaleSlopeAndInterceptAtOffset(offset) { + return rsis[offset]; + } - // update RSI flags - isIdentityRSI = true; - isConstantRSI = true; - for (var s = 0, lens = rsis.length; s < lens; ++s) { - if (!rsis[s].isID()) { - isIdentityRSI = false; + /** + * Set the rescale slope and intercept. + * + * @param {object} inRsi The input rescale slope and intercept. + * @param {number} offset The rsi offset (only needed for non constant rsi). + */ + this.setRescaleSlopeAndIntercept = function (inRsi, offset) { + // update identity flag + isIdentityRSI = isIdentityRSI && inRsi.isID(); + // update constant flag + if (!isConstantRSI) { + if (typeof index === 'undefined') { + throw new Error( + 'Cannot store non constant RSI with empty slice index.'); } - if (s > 0 && !rsis[s].equals(rsis[s - 1])) { - isConstantRSI = false; + rsis.splice(offset, 0, inRsi); + } else { + if (!rsi.equals(inRsi)) { + if (typeof index === 'undefined') { + // no slice index, replace existing + rsi = inRsi; + } else { + // first non constant rsi + isConstantRSI = false; + // switch to non constant mode + rsis = []; + // initialise RSIs + for (var i = 0, leni = getSecondaryOffsetMax(); i < leni; ++i) { + rsis.push(i); + } + // store + rsi = null; + rsis.splice(offset, 0, inRsi); + } } } }; @@ -271,11 +342,10 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { * Get value at offset. Warning: No size check... * * @param {number} offset The desired offset. - * @param {number} frame The desired frame. * @returns {number} The value at offset. */ - this.getValueAtOffset = function (offset, frame) { - return buffer[frame][offset]; + this.getValueAtOffset = function (offset) { + return buffer[offset]; }; /** @@ -285,16 +355,17 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { */ this.clone = function () { // clone the image buffer - var clonedBuffer = []; - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - clonedBuffer[f] = buffer[f].slice(0); - } + var clonedBuffer = buffer.slice(0); // create the image copy - var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer); - // copy the RSIs - var nslices = this.getGeometry().getSize().getNumberOfSlices(); - for (var k = 0; k < nslices; ++k) { - copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept(k), k); + var copy = new dwv.image.Image(this.getGeometry(), clonedBuffer, imageUids); + // copy the RSI(s) + if (this.isConstantRSI()) { + copy.setRescaleSlopeAndIntercept(this.getRescaleSlopeAndIntercept()); + } else { + for (var i = 0; i < getSecondaryOffsetMax(); ++i) { + copy.setRescaleSlopeAndIntercept( + getRescaleSlopeAndInterceptAtOffset(i), i); + } } // copy extras copy.setPhotometricInterpretation(this.getPhotometricInterpretation()); @@ -304,27 +375,51 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { return copy; }; + /** + * Re-allocate buffer memory to an input size. + * + * @param {number} size The new size. + */ + function realloc(size) { + // save buffer + var tmpBuffer = buffer; + // create new + buffer = dwv.dicom.getTypedArray( + buffer.BYTES_PER_ELEMENT * 8, + meta.IsSigned ? 1 : 0, + size); + if (buffer === null) { + throw new Error('Cannot reallocate data for image.'); + } + // put old in new + buffer.set(tmpBuffer); + // clean + tmpBuffer = null; + } + /** * Append a slice to the image. * * @param {Image} rhs The slice to append. - * @param {number} frame The frame where to append. - * @returns {number} The number of the inserted slice. + * @param {number} timeId An optional time ID. */ - this.appendSlice = function (rhs, frame) { + this.appendSlice = function (rhs, timeId) { + if (typeof timeId === 'undefined') { + timeId = 0; + } // check input if (rhs === null) { throw new Error('Cannot append null slice'); } var rhsSize = rhs.getGeometry().getSize(); var size = geometry.getSize(); - if (rhsSize.getNumberOfSlices() !== 1) { + if (rhsSize.get(2) !== 1) { throw new Error('Cannot append more than one slice'); } - if (size.getNumberOfColumns() !== rhsSize.getNumberOfColumns()) { + if (size.get(0) !== rhsSize.get(0)) { throw new Error('Cannot append a slice with different number of columns'); } - if (size.getNumberOfRows() !== rhsSize.getNumberOfRows()) { + if (size.get(1) !== rhsSize.get(1)) { throw new Error('Cannot append a slice with different number of rows'); } if (!geometry.getOrientation().equals( @@ -345,47 +440,73 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { } } - var f = (typeof frame === 'undefined') ? 0 : frame; - // calculate slice size - var mul = 1; - if (photometricInterpretation === 'RGB' || - photometricInterpretation === 'YBR_FULL') { - mul = 3; + var sliceSize = numberOfComponents * size.getDimSize(2); + + // create full buffer if not done yet + if (typeof meta.numberOfFiles === 'undefined') { + throw new Error('Missing number of files for buffer manipulation.'); + } + var fullBufferSize = sliceSize * meta.numberOfFiles; + if (size.length() === 4) { + fullBufferSize *= size.get(3); + } + if (buffer.length !== fullBufferSize) { + realloc(fullBufferSize); } - var sliceSize = mul * size.getSliceSize(); - // create the new buffer - var newBuffer = dwv.dicom.getTypedArray( - buffer[f].BYTES_PER_ELEMENT * 8, - meta.IsSigned ? 1 : 0, - sliceSize * (size.getNumberOfSlices() + 1)); - - // append slice at new position - var newSliceNb = geometry.getSliceIndex(rhs.getGeometry().getOrigin()); - if (newSliceNb === 0) { - newBuffer.set(rhs.getFrame(f)); - newBuffer.set(buffer[f], sliceSize); - } else if (newSliceNb === size.getNumberOfSlices()) { - newBuffer.set(buffer[f]); - newBuffer.set(rhs.getFrame(f), size.getNumberOfSlices() * sliceSize); - } else { - var offset = newSliceNb * sliceSize; - newBuffer.set(buffer[f].subarray(0, offset - 1)); - newBuffer.set(rhs.getFrame(f), offset); - newBuffer.set(buffer[f].subarray(offset), offset + sliceSize); + var newSliceIndex = geometry.getSliceIndex(rhs.getGeometry().getOrigin()); + var values = new Array(geometry.getSize().length()); + values.fill(0); + values[2] = newSliceIndex; + if (size.length() === 4) { + values[3] = timeId; + } + var index = new dwv.math.Index(values); + var primaryOffset = size.indexToOffset(index); + var secondaryOffset = this.getSecondaryOffset(index); + + // first frame special slice by slice append + if (timeId === 0) { + // store slice + var oldNumberOfSlices = size.get(2); + // move content if needed + var start = primaryOffset; + var end; + if (newSliceIndex === 0) { + // insert slice before current data + end = start + oldNumberOfSlices * sliceSize; + buffer.set( + buffer.subarray(start, end), + primaryOffset + sliceSize + ); + } else if (newSliceIndex < oldNumberOfSlices) { + // insert slice in between current data + end = start + (oldNumberOfSlices - newSliceIndex) * sliceSize; + buffer.set( + buffer.subarray(start, end), + primaryOffset + sliceSize + ); + } } + // add new slice content + buffer.set(rhs.getBuffer(), primaryOffset); + // update geometry - geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceNb); + if (timeId === 0) { + geometry.appendOrigin(rhs.getGeometry().getOrigin(), newSliceIndex); + } // update rsi - rsis.splice(newSliceNb, 0, rhs.getRescaleSlopeAndIntercept(0)); + // (rhs should just have one rsi) + this.setRescaleSlopeAndIntercept( + rhs.getRescaleSlopeAndIntercept(), secondaryOffset); - // copy to class variables - buffer[f] = newBuffer; + // current number of images + var numberOfImages = imageUids.length; // insert sop instance UIDs - imageUids.splice(newSliceNb, 0, rhs.getImageUids()[0]); + imageUids.splice(secondaryOffset, 0, rhs.getImageUid()); // update window presets if (typeof meta.windowPresets !== 'undefined') { @@ -395,33 +516,70 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { var pkey = null; for (var i = 0; i < keys.length; ++i) { pkey = keys[i]; - if (typeof windowPresets[pkey] !== 'undefined') { - if (typeof windowPresets[pkey].perslice !== 'undefined' && - windowPresets[pkey].perslice === true) { - // use first new preset wl... + var rhsPreset = rhsPresets[pkey]; + var windowPreset = windowPresets[pkey]; + if (typeof windowPreset !== 'undefined') { + // if not set or false, check perslice + if (typeof windowPreset.perslice === 'undefined' || + windowPreset.perslice === false) { + // if different preset.wl, mark it as perslice + if (!windowPreset.wl[0].equals(rhsPreset.wl[0])) { + windowPreset.perslice = true; + // fill wl array with copy of wl[0] + // (loop on number of images minus the existing one) + for (var j = 0; j < numberOfImages - 1; ++j) { + windowPreset.wl.push(windowPreset.wl[0]); + } + } + } + // store (first) rhs preset.wl if needed + if (typeof windowPreset.perslice !== 'undefined' && + windowPreset.perslice === true) { windowPresets[pkey].wl.splice( - newSliceNb, 0, rhsPresets[pkey].wl[0]); - } else { - windowPresets[pkey] = rhsPresets[pkey]; + secondaryOffset, 0, rhsPreset.wl[0]); } } else { - // update + // if not defined (it should be), store all windowPresets[pkey] = rhsPresets[pkey]; } } } - - // return the appended slice number - return newSliceNb; }; /** * Append a frame buffer to the image. * * @param {object} frameBuffer The frame buffer to append. + * @param {number} frameIndex The frame index. */ - this.appendFrameBuffer = function (frameBuffer) { - buffer.push(frameBuffer); + this.appendFrameBuffer = function (frameBuffer, frameIndex) { + // create full buffer if not done yet + var size = geometry.getSize(); + var frameSize = numberOfComponents * size.getDimSize(2); + if (typeof meta.numberOfFiles === 'undefined') { + throw new Error('Missing number of files for frame buffer manipulation.'); + } + var fullBufferSize = frameSize * meta.numberOfFiles; + if (buffer.length !== fullBufferSize) { + realloc(fullBufferSize); + } + // append + if (frameIndex >= meta.numberOfFiles) { + throw new Error( + 'Cannot append a frame at an index above the number of frames'); + } + buffer.set(frameBuffer, frameSize * frameIndex); + // update geometry + this.appendFrame(); + }; + + /** + * Append a frame to the image. + */ + this.appendFrame = function () { + geometry.appendFrame(); + fireEvent({type: 'appendframe'}); + // memory will be updated at the first appendSlice or appendFrameBuffer }; /** @@ -462,6 +620,38 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { } return histogram; }; + + /** + * Add an event listener to this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. + */ + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + + /** + * Remove an event listener from this class. + * + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. + */ + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); + }; + + /** + * Fire an event: call all associated listeners with the input event object. + * + * @param {object} event The event to fire. + * @private + */ + function fireEvent(event) { + listenerHandler.fireEvent(event); + } }; /** @@ -476,12 +666,25 @@ dwv.image.Image = function (geometry, buffer, numberOfFrames, imageUids) { */ dwv.image.Image.prototype.getValue = function (i, j, k, f) { var frame = (f || 0); - var index = new dwv.math.Index3D(i, j, k); - return this.getValueAtOffset(this.getGeometry().indexToOffset(index), frame); + var index = new dwv.math.Index([i, j, k, frame]); + return this.getValueAtOffset( + this.getGeometry().getSize().indexToOffset(index)); +}; + +/** + * Get the value of the image at a specific index. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getValueAtIndex = function (index) { + return this.getValueAtOffset( + this.getGeometry().getSize().indexToOffset(index)); }; /** - * Get the rescaled value of the image at a specific coordinate. + * Get the rescaled value of the image at a specific position. * * @param {number} i The X index. * @param {number} j The Y index. @@ -491,28 +694,51 @@ dwv.image.Image.prototype.getValue = function (i, j, k, f) { * Warning: No size check... */ dwv.image.Image.prototype.getRescaledValue = function (i, j, k, f) { - var frame = (f || 0); - var val = this.getValue(i, j, k, frame); + if (typeof f === 'undefined') { + f = 0; + } + var val = this.getValue(i, j, k, f); if (!this.isIdentityRSI()) { - val = this.getRescaleSlopeAndIntercept(k).apply(val); + if (this.isConstantRSI()) { + val = this.getRescaleSlopeAndIntercept().apply(val); + } else { + var values = [i, j, k, f]; + var index = new dwv.math.Index(values); + val = this.getRescaleSlopeAndIntercept(index).apply(val); + } } return val; }; +/** + * Get the rescaled value of the image at a specific index. + * + * @param {dwv.math.Index} index The index. + * @returns {number} The rescaled value at the desired position. + * Warning: No size check... + */ +dwv.image.Image.prototype.getRescaledValueAtIndex = function (index) { + return this.getRescaledValueAtOffset( + this.getGeometry().getSize().indexToOffset(index) + ); +}; + /** * Get the rescaled value of the image at a specific offset. * * @param {number} offset The desired offset. - * @param {number} k The Z index. - * @param {number} f The frame number. * @returns {number} The rescaled value at the desired offset. * Warning: No size check... */ -dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset, k, f) { - var frame = (f || 0); - var val = this.getValueAtOffset(offset, frame); +dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset) { + var val = this.getValueAtOffset(offset); if (!this.isIdentityRSI()) { - val = this.getRescaleSlopeAndIntercept(k).apply(val); + if (this.isConstantRSI()) { + val = this.getRescaleSlopeAndIntercept().apply(val); + } else { + var index = this.getGeometry().getSize().offsetToIndex(offset); + val = this.getRescaleSlopeAndIntercept(index).apply(val); + } } return val; }; @@ -524,20 +750,22 @@ dwv.image.Image.prototype.getRescaledValueAtOffset = function (offset, k, f) { * @returns {object} The range {min, max}. */ dwv.image.Image.prototype.calculateDataRange = function () { - var size = this.getGeometry().getSize().getTotalSize(); - var nFrames = 1; //this.getNumberOfFrames(); - var min = this.getValueAtOffset(0, 0); + var min = this.getValueAtOffset(0); var max = min; var value = 0; - for (var f = 0; f < nFrames; ++f) { - for (var i = 0; i < size; ++i) { - value = this.getValueAtOffset(i, f); - if (value > max) { - max = value; - } - if (value < min) { - min = value; - } + var size = this.getGeometry().getSize(); + var leni = size.getTotalSize(); + // max to 3D + if (size.length() >= 3) { + leni = size.getDimSize(3); + } + for (var i = 0; i < leni; ++i) { + value = this.getValueAtOffset(i); + if (value > max) { + max = value; + } + if (value < min) { + min = value; } } // return @@ -555,31 +783,29 @@ dwv.image.Image.prototype.calculateRescaledDataRange = function () { return this.getDataRange(); } else if (this.isConstantRSI()) { var range = this.getDataRange(); - var resmin = this.getRescaleSlopeAndIntercept(0).apply(range.min); - var resmax = this.getRescaleSlopeAndIntercept(0).apply(range.max); + var resmin = this.getRescaleSlopeAndIntercept().apply(range.min); + var resmax = this.getRescaleSlopeAndIntercept().apply(range.max); return { min: ((resmin < resmax) ? resmin : resmax), max: ((resmin > resmax) ? resmin : resmax) }; } else { - var size = this.getGeometry().getSize(); - var nFrames = 1; //this.getNumberOfFrames(); - var rmin = this.getRescaledValue(0, 0, 0); + var rmin = this.getRescaledValueAtOffset(0); var rmax = rmin; var rvalue = 0; - for (var f = 0, nframes = nFrames; f < nframes; ++f) { - for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) { - for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) { - for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) { - rvalue = this.getRescaledValue(i, j, k, f); - if (rvalue > rmax) { - rmax = rvalue; - } - if (rvalue < rmin) { - rmin = rvalue; - } - } - } + var size = this.getGeometry().getSize(); + var leni = size.getTotalSize(); + // max to 3D + if (size.length() === 3) { + leni = size.getDimSize(3); + } + for (var i = 0; i < leni; ++i) { + rvalue = this.getRescaledValueAtOffset(i); + if (rvalue > rmax) { + rmax = rvalue; + } + if (rvalue < rmin) { + rmin = rvalue; } } // return @@ -595,34 +821,28 @@ dwv.image.Image.prototype.calculateRescaledDataRange = function () { dwv.image.Image.prototype.calculateHistogram = function () { var size = this.getGeometry().getSize(); var histo = []; - var min = this.getValue(0, 0, 0); + var min = this.getValueAtOffset(0); var max = min; var value = 0; - var rmin = this.getRescaledValue(0, 0, 0); + var rmin = this.getRescaledValueAtOffset(0); var rmax = rmin; var rvalue = 0; - for (var f = 0, nframes = this.getNumberOfFrames(); f < nframes; ++f) { - for (var k = 0, nslices = size.getNumberOfSlices(); k < nslices; ++k) { - for (var j = 0, nrows = size.getNumberOfRows(); j < nrows; ++j) { - for (var i = 0, ncols = size.getNumberOfColumns(); i < ncols; ++i) { - value = this.getValue(i, j, k, f); - if (value > max) { - max = value; - } - if (value < min) { - min = value; - } - rvalue = this.getRescaleSlopeAndIntercept(k).apply(value); - if (rvalue > rmax) { - rmax = rvalue; - } - if (rvalue < rmin) { - rmin = rvalue; - } - histo[rvalue] = (histo[rvalue] || 0) + 1; - } - } + for (var i = 0, leni = size.getTotalSize(); i < leni; ++i) { + value = this.getValueAtOffset(i); + if (value > max) { + max = value; + } + if (value < min) { + min = value; + } + rvalue = this.getRescaledValueAtOffset(i); + if (rvalue > rmax) { + rmax = rvalue; } + if (rvalue < rmin) { + rmin = rvalue; + } + histo[rvalue] = (histo[rvalue] || 0) + 1; } // set data range var dataRange = {min: min, max: max}; @@ -643,9 +863,10 @@ dwv.image.Image.prototype.calculateHistogram = function () { /** * Convolute the image with a given 2D kernel. * + * Note: Uses raw buffer values. + * * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix. * @returns {Image} The convoluted image. - * Note: Uses the raw buffer values. */ dwv.image.Image.prototype.convolute2D = function (weights) { if (weights.length !== 9) { @@ -658,22 +879,38 @@ dwv.image.Image.prototype.convolute2D = function (weights) { var newBuffer = newImage.getBuffer(); var imgSize = this.getGeometry().getSize(); - var ncols = imgSize.getNumberOfColumns(); - var nrows = imgSize.getNumberOfRows(); - var nslices = imgSize.getNumberOfSlices(); - var nframes = this.getNumberOfFrames(); + var dimOffset = imgSize.getDimSize(2) * this.getNumberOfComponents(); + for (var k = 0; k < imgSize.get(2); ++k) { + this.convoluteBuffer(weights, newBuffer, k * dimOffset); + } + + return newImage; +}; + +/** + * Convolute an image buffer with a given 2D kernel. + * + * Note: Uses raw buffer values. + * + * @param {Array} weights The weights of the 2D kernel as a 3x3 matrix. + * @param {Array} buffer The buffer to convolute. + * @param {number} startOffset The index to start at. + */ +dwv.image.Image.prototype.convoluteBuffer = function ( + weights, buffer, startOffset) { + var imgSize = this.getGeometry().getSize(); + var ncols = imgSize.get(0); + var nrows = imgSize.get(1); var ncomp = this.getNumberOfComponents(); - // adapt to number of component and planar configuration + // number of component and planar configuration vars var factor = 1; var componentOffset = 1; - var frameOffset = imgSize.getTotalSize(); if (ncomp === 3) { - frameOffset *= 3; if (this.getPlanarConfiguration() === 0) { factor = 3; } else { - componentOffset = imgSize.getTotalSize(); + componentOffset = imgSize.getDimSize(2); } } @@ -743,54 +980,46 @@ dwv.image.Image.prototype.convolute2D = function (weights) { /*jshint indent:4 */ // loop vars - var pixelOffset = 0; + var pixelOffset = startOffset; var newValue = 0; var wOffFinal = []; - // go through the destination image pixels - for (var f = 0; f < nframes; f++) { - pixelOffset = f * frameOffset; - for (var c = 0; c < ncomp; c++) { - // special component offset - pixelOffset += c * componentOffset; - for (var k = 0; k < nslices; k++) { - for (var j = 0; j < nrows; j++) { - for (var i = 0; i < ncols; i++) { - wOffFinal = wOff; - // special border cases - if (i === 0 && j === 0) { - wOffFinal = wOff00; - } else if (i === 0 && j === (nrows - 1)) { - wOffFinal = wOff0n; - } else if (i === (ncols - 1) && j === 0) { - wOffFinal = wOffn0; - } else if (i === (ncols - 1) && j === (nrows - 1)) { - wOffFinal = wOffnn; - } else if (i === 0 && j !== (nrows - 1) && j !== 0) { - wOffFinal = wOff0x; - } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) { - wOffFinal = wOffnx; - } else if (i !== 0 && i !== (ncols - 1) && j === 0) { - wOffFinal = wOffx0; - } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) { - wOffFinal = wOffxn; - } - - // calculate the weighed sum of the source image pixels that - // fall under the convolution matrix - newValue = 0; - for (var wi = 0; wi < 9; ++wi) { - newValue += this.getValueAtOffset( - pixelOffset + wOffFinal[wi], f) * weights[wi]; - } - newBuffer[f][pixelOffset] = newValue; - // increment pixel offset - pixelOffset += factor; - } + for (var c = 0; c < ncomp; ++c) { + // component offset + pixelOffset += c * componentOffset; + for (var j = 0; j < nrows; ++j) { + for (var i = 0; i < ncols; ++i) { + wOffFinal = wOff; + // special border cases + if (i === 0 && j === 0) { + wOffFinal = wOff00; + } else if (i === 0 && j === (nrows - 1)) { + wOffFinal = wOff0n; + } else if (i === (ncols - 1) && j === 0) { + wOffFinal = wOffn0; + } else if (i === (ncols - 1) && j === (nrows - 1)) { + wOffFinal = wOffnn; + } else if (i === 0 && j !== (nrows - 1) && j !== 0) { + wOffFinal = wOff0x; + } else if (i === (ncols - 1) && j !== (nrows - 1) && j !== 0) { + wOffFinal = wOffnx; + } else if (i !== 0 && i !== (ncols - 1) && j === 0) { + wOffFinal = wOffx0; + } else if (i !== 0 && i !== (ncols - 1) && j === (nrows - 1)) { + wOffFinal = wOffxn; + } + // calculate the weighed sum of the source image pixels that + // fall under the convolution matrix + newValue = 0; + for (var wi = 0; wi < 9; ++wi) { + newValue += this.getValueAtOffset( + pixelOffset + wOffFinal[wi]) * weights[wi]; } + buffer[pixelOffset] = newValue; + // increment pixel offset + pixelOffset += factor; } } } - return newImage; }; /** @@ -804,10 +1033,8 @@ dwv.image.Image.prototype.convolute2D = function (weights) { dwv.image.Image.prototype.transform = function (operator) { var newImage = this.clone(); var newBuffer = newImage.getBuffer(); - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) { - newBuffer[f][i] = operator(newImage.getValueAtOffset(i, f)); - } + for (var i = 0, leni = newBuffer.length; i < leni; ++i) { + newBuffer[i] = operator(newImage.getValueAtOffset(i)); } return newImage; }; @@ -824,14 +1051,12 @@ dwv.image.Image.prototype.transform = function (operator) { dwv.image.Image.prototype.compose = function (rhs, operator) { var newImage = this.clone(); var newBuffer = newImage.getBuffer(); - for (var f = 0, lenf = this.getNumberOfFrames(); f < lenf; ++f) { - for (var i = 0, leni = newBuffer[f].length; i < leni; ++i) { - // using the operator on the local buffer, i.e. the - // latest (not original) data - newBuffer[f][i] = Math.floor( - operator(this.getValueAtOffset(i, f), rhs.getValueAtOffset(i, f)) - ); - } + for (var i = 0, leni = newBuffer.length; i < leni; ++i) { + // using the operator on the local buffer, i.e. the + // latest (not original) data + newBuffer[i] = Math.floor( + operator(this.getValueAtOffset(i), rhs.getValueAtOffset(i)) + ); } return newImage; }; diff --git a/src/image/imageFactory.js b/src/image/imageFactory.js index 8db8817ad5..b35c9c3d94 100644 --- a/src/image/imageFactory.js +++ b/src/image/imageFactory.js @@ -9,15 +9,41 @@ dwv.image = dwv.image || {}; */ dwv.image.ImageFactory = function () {}; +/** + * {@link dwv.image.Image} factory. Defaults to local one. + * + * @see dwv.image.ImageFactory + */ +dwv.ImageFactory = dwv.image.ImageFactory; + +/** + * Check dicom elements. Throws an error if not suitable. + * + * @param {object} dicomElements The DICOM tags. + */ +dwv.image.ImageFactory.prototype.checkElements = function (dicomElements) { + // columns + var columns = dicomElements.getFromKey('x00280011'); + if (!columns) { + throw new Error('Missing or empty DICOM image number of columns'); + } + // rows + var rows = dicomElements.getFromKey('x00280010'); + if (!rows) { + throw new Error('Missing or empty DICOM image number of rows'); + } +}; + /** * Get an {@link dwv.image.Image} object from the read DICOM file. * * @param {object} dicomElements The DICOM tags. * @param {Array} pixelBuffer The pixel buffer. + * @param {number} numberOfFiles The input number of files. * @returns {dwv.image.Image} A new Image. */ dwv.image.ImageFactory.prototype.create = function ( - dicomElements, pixelBuffer) { + dicomElements, pixelBuffer, numberOfFiles) { // columns var columns = dicomElements.getFromKey('x00280011'); if (!columns) { @@ -28,27 +54,20 @@ dwv.image.ImageFactory.prototype.create = function ( if (!rows) { throw new Error('Missing or empty DICOM image number of rows'); } - // image size - var size = new dwv.image.Size(columns, rows); - // spacing - var rowSpacing = null; - var columnSpacing = null; - // PixelSpacing - var pixelSpacing = dicomElements.getFromKey('x00280030'); - // ImagerPixelSpacing - var imagerPixelSpacing = dicomElements.getFromKey('x00181164'); - if (pixelSpacing && pixelSpacing[0] && pixelSpacing[1]) { - rowSpacing = parseFloat(pixelSpacing[0]); - columnSpacing = parseFloat(pixelSpacing[1]); - } else if (imagerPixelSpacing && - imagerPixelSpacing[0] && - imagerPixelSpacing[1]) { - rowSpacing = parseFloat(imagerPixelSpacing[0]); - columnSpacing = parseFloat(imagerPixelSpacing[1]); + var sizeValues = [columns, rows, 1]; + + // frames + var frames = dicomElements.getFromKey('x00280008'); + if (frames) { + sizeValues.push(frames); } + + // image size + var size = new dwv.image.Size(sizeValues); + // image spacing - var spacing = new dwv.image.Spacing(columnSpacing, rowSpacing); + var spacing = dicomElements.getPixelSpacing(); // TransferSyntaxUID var transferSyntaxUID = dicomElements.getFromKey('x00020010'); @@ -59,22 +78,16 @@ dwv.image.ImageFactory.prototype.create = function ( // ImagePositionPatient var imagePositionPatient = dicomElements.getFromKey('x00200032'); - // InstanceNumber - var instanceNumber = dicomElements.getFromKey('x00200013'); - // slice position var slicePosition = new Array(0, 0, 0); if (imagePositionPatient) { slicePosition = [parseFloat(imagePositionPatient[0]), parseFloat(imagePositionPatient[1]), parseFloat(imagePositionPatient[2])]; - } else if (instanceNumber) { - // use instanceNumber as slice index if no imagePositionPatient was provided - dwv.logger.warn('Using instanceNumber as imagePositionPatient.'); - slicePosition[2] = parseInt(instanceNumber, 10); } - // slice orientation + // slice orientation (cosines are matrices' columns) + // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html#sect_C.7.6.2.1.1 var imageOrientationPatient = dicomElements.getFromKey('x00200037'); var orientationMatrix; if (imageOrientationPatient) { @@ -87,10 +100,13 @@ dwv.image.ImageFactory.prototype.create = function ( parseFloat(imageOrientationPatient[4]), parseFloat(imageOrientationPatient[5])); var normal = rowCosines.crossProduct(colCosines); - orientationMatrix = new dwv.math.Matrix33( - rowCosines.getX(), rowCosines.getY(), rowCosines.getZ(), - colCosines.getX(), colCosines.getY(), colCosines.getZ(), - normal.getX(), normal.getY(), normal.getZ()); + /* eslint-disable array-element-newline */ + orientationMatrix = new dwv.math.Matrix33([ + rowCosines.getX(), colCosines.getX(), normal.getX(), + rowCosines.getY(), colCosines.getY(), normal.getY(), + rowCosines.getZ(), colCosines.getZ(), normal.getZ() + ]); + /* eslint-enable array-element-newline */ } // geometry @@ -103,10 +119,27 @@ dwv.image.ImageFactory.prototype.create = function ( var sopInstanceUid = dwv.dicom.cleanString( dicomElements.getFromKey('x00080018')); + // Sample per pixels + var samplesPerPixel = dicomElements.getFromKey('x00280002'); + if (!samplesPerPixel) { + samplesPerPixel = 1; + } + + // check buffer size + var bufferSize = size.getTotalSize() * samplesPerPixel; + if (bufferSize !== pixelBuffer.length) { + dwv.logger.warn('Badly sized pixel buffer: ' + + pixelBuffer.length + ' != ' + bufferSize); + if (bufferSize < pixelBuffer.length) { + pixelBuffer = pixelBuffer.slice(0, size.getTotalSize()); + } else { + throw new Error('Underestimated buffer size, can\'t fix it...'); + } + } + // image - var image = new dwv.image.Image( - geometry, pixelBuffer, pixelBuffer.length, [sopInstanceUid]); - // PhotometricInterpretation + var image = new dwv.image.Image(geometry, pixelBuffer, [sopInstanceUid]); + // PhotometricInterpretation var photometricInterpretation = dicomElements.getFromKey('x00280004'); if (photometricInterpretation) { var photo = dwv.dicom.cleanString(photometricInterpretation).toUpperCase(); @@ -116,7 +149,6 @@ dwv.image.ImageFactory.prototype.create = function ( photo = 'RGB'; } // check samples per pixels - var samplesPerPixel = parseInt(dicomElements.getFromKey('x00280002'), 10); if (photo === 'RGB' && samplesPerPixel === 1) { photo = 'PALETTE COLOR'; } @@ -146,6 +178,8 @@ dwv.image.ImageFactory.prototype.create = function ( // meta information var meta = {}; + // data length + meta.numberOfFiles = numberOfFiles; // Modality var modality = dicomElements.getFromKey('x00080060'); if (modality) { @@ -172,6 +206,12 @@ dwv.image.ImageFactory.prototype.create = function ( if (pixelRepresentation) { meta.IsSigned = (pixelRepresentation === 1); } + // PatientPosition + var patientPosition = dicomElements.getFromKey('x00185100'); + meta.PatientPosition = false; + if (patientPosition) { + meta.PatientPosition = patientPosition; + } // window level presets var windowPresets = {}; @@ -193,8 +233,7 @@ dwv.image.ImageFactory.prototype.create = function ( } windowPresets[name] = { wl: [new dwv.image.WindowLevel(center, width)], - name: name, - perslice: true + name: name }; } if (width === 0) { diff --git a/src/image/iterator.js b/src/image/iterator.js index ed1dbf3eed..a8d94d662f 100644 --- a/src/image/iterator.js +++ b/src/image/iterator.js @@ -3,7 +3,7 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; /** - * Get an iterator for a given range for a one component data. + * Get an simple iterator for a given range for a one component data. * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols * @param {Function} dataAccessor Function to access data. @@ -12,7 +12,7 @@ dwv.image = dwv.image || {}; * @param {number} increment The increment between indicies (default=1). * @returns {object} An iterator folowing the iterator and iterable protocol. */ -dwv.image.range = function (dataAccessor, start, end, increment) { +dwv.image.simpleRange = function (dataAccessor, start, end, increment) { if (typeof increment === 'undefined') { increment = 1; } @@ -36,6 +36,82 @@ dwv.image.range = function (dataAccessor, start, end, increment) { }; }; +/** + * Get an iterator for a given range for a one component data. + * + * Using 'maxIter' and not an 'end' index since it fails in some edge cases + * (for ex coronal2, ie zxy) + * + * @param {Function} dataAccessor Function to access data. + * @param {number} start Zero-based index at which to start the iteration. + * @param {number} maxIter The maximum number of iterations. + * @param {number} increment Increment between indicies. + * @param {number} blockMaxIter Number of applied increment after which + * blockIncrement is applied. + * @param {number} blockIncrement Increment after blockMaxIter is reached, + * the value is from block start to the next block start. + * @param {boolean} reverse1 If true, loop from end to start. + * WARN: don't forget to set the value of start as the last index! + * @param {boolean} reverse2 If true, loop from block end to block start. + * @returns {object} An iterator folowing the iterator and iterable protocol. + */ +dwv.image.range = function (dataAccessor, start, maxIter, increment, + blockMaxIter, blockIncrement, reverse1, reverse2) { + if (typeof reverse1 === 'undefined') { + reverse1 = false; + } + if (typeof reverse2 === 'undefined') { + reverse2 = false; + } + + // first index of the iteration + var nextIndex = start; + // adapt first index and increments to reverse values + if (reverse1) { + blockIncrement *= -1; + if (reverse2) { + // start at end of line + nextIndex -= (blockMaxIter - 1) * increment; + } else { + increment *= -1; + } + } else { + if (reverse2) { + // start at end of line + nextIndex += (blockMaxIter - 1) * increment; + increment *= -1; + } + } + var finalBlockIncrement = blockIncrement - blockMaxIter * increment; + + // counters + var mainCount = 0; + var blockCount = 0; + // result + return { + next: function () { + if (mainCount < maxIter) { + var result = { + value: dataAccessor(nextIndex), + done: false + }; + nextIndex += increment; + ++mainCount; + ++blockCount; + if (blockCount === blockMaxIter) { + blockCount = 0; + nextIndex += finalBlockIncrement; + } + return result; + } + return { + done: true, + index: nextIndex + }; + } + }; +}; + /** * Get an iterator for a given range with bounds (for a one component data). * @@ -198,22 +274,102 @@ dwv.image.getIteratorValues = function (iterator) { /** * Get a slice index iterator. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. + * @param {boolean} isRescaled Flag for rescaled values (default false). + * @param {dwv.math.Matrix33} viewOrientation The view orientation. * @returns {object} The slice iterator. */ -dwv.image.getSliceIterator = function (image, slice, frame) { - var sliceSize = image.getGeometry().getSize().getSliceSize(); - var start = slice * sliceSize; - - var dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); +dwv.image.getSliceIterator = function ( + image, position, isRescaled, viewOrientation) { + var size = image.getGeometry().getSize(); + // zero-ify non direction index + var dirMax2Index = 2; + if (viewOrientation && typeof viewOrientation !== 'undefined') { + dirMax2Index = viewOrientation.getColAbsMax(2).index; + } + var posValues = position.getValues(); + // keep the main direction and any other than 3D + var indexFilter = function (element, index) { + return (index === dirMax2Index || index > 2) ? element : 0; }; + var posStart = new dwv.math.Index(posValues.map(indexFilter)); + var start = size.indexToOffset(posStart); + + // default to non rescaled data + if (typeof isRescaled === 'undefined') { + isRescaled = false; + } + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var ncols = size.get(0); + var nrows = size.get(1); + var nslices = size.get(2); + var sliceSize = size.getDimSize(2); var range = null; if (image.getNumberOfComponents() === 1) { - range = dwv.image.range(dataAccessor, start, start + sliceSize); + if (viewOrientation && typeof viewOrientation !== 'undefined') { + var dirMax0 = viewOrientation.getColAbsMax(0); + var dirMax2 = viewOrientation.getColAbsMax(2); + + // default reverse + var reverse1 = false; + var reverse2 = false; + + var maxIter = null; + if (dirMax2.index === 2) { + // axial + maxIter = ncols * nrows; + if (dirMax0.index === 0) { + // xyz + range = dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, ncols, reverse1, reverse2); + } else { + // yxz + range = dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, 1, reverse1, reverse2); + } + } else if (dirMax2.index === 0) { + // sagittal + maxIter = nslices * nrows; + if (dirMax0.index === 1) { + // yzx + range = dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, sliceSize, reverse1, reverse2); + } else { + // zyx + range = dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, ncols, reverse1, reverse2); + } + } else if (dirMax2.index === 1) { + // coronal + maxIter = nslices * ncols; + if (dirMax0.index === 0) { + // xzy + range = dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, sliceSize, reverse1, reverse2); + } else { + // zxy + range = dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, 1, reverse1, reverse2); + } + } else { + throw new Error('Unknown direction: ' + dirMax2.index); + } + } else { + // default case + range = dwv.image.simpleRange(dataAccessor, start, start + sliceSize); + } } else if (image.getNumberOfComponents() === 3) { // 3 times bigger... start *= 3; @@ -232,56 +388,56 @@ dwv.image.getSliceIterator = function (image, slice, frame) { /** * Get a slice index iterator for a rectangular region. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. * @param {boolean} isRescaled Flag for rescaled values (default false). * @param {dwv.math.Point2D} min The minimum position (optional). * @param {dwv.math.Point2D} max The maximum position (optional). * @returns {object} The slice iterator. */ dwv.image.getRegionSliceIterator = function ( - image, slice, frame, isRescaled, min, max) { + image, position, isRescaled, min, max) { if (image.getNumberOfComponents() !== 1) { throw new Error('Unsupported number of components for region iterator: ' + image.getNumberOfComponents()); } + // default to non rescaled data if (typeof isRescaled === 'undefined') { isRescaled = false; } - var geometry = image.getGeometry(); - var size = geometry.getSize(); + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var size = image.getGeometry().getSize(); if (typeof min === 'undefined') { min = new dwv.math.Point2D(0, 0); } if (typeof max === 'undefined') { max = new dwv.math.Point2D( - size.getNumberOfColumns() - 1, - size.getNumberOfRows() + size.get(0) - 1, + size.get(1) ); } // position to pixel for max: extra X is ok, remove extra Y - var minIndex = new dwv.math.Index3D(min.getX(), min.getY(), slice); - var startOffset = geometry.indexToOffset(minIndex); - var maxIndex = new dwv.math.Index3D(max.getX(), max.getY() - 1, slice); - var endOffset = geometry.indexToOffset(maxIndex); + var startOffset = size.indexToOffset(position.getWithNew2D( + min.getX(), min.getY() + )); + var endOffset = size.indexToOffset(position.getWithNew2D( + max.getX(), max.getY() - 1 + )); // minimum 1 column var rangeNumberOfColumns = Math.max(1, max.getX() - min.getX()); - var rowIncrement = size.getNumberOfColumns() - rangeNumberOfColumns; - - // data accessor - var dataAccessor = null; - if (isRescaled) { - dataAccessor = function (offset) { - return image.getRescaledValueAtOffset(offset, slice, frame); - }; - } else { - dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); - }; - } + var rowIncrement = size.get(0) - rangeNumberOfColumns; return dwv.image.rangeRegion( dataAccessor, startOffset, endOffset + 1, @@ -291,25 +447,35 @@ dwv.image.getRegionSliceIterator = function ( /** * Get a slice index iterator for a rectangular region. * - * @param {object} image The image to parse. - * @param {number} slice The index of the slice. - * @param {number} frame The frame index. + * @param {dwv.image.Image} image The image to parse. + * @param {dwv.math.Point} position The current position. * @param {boolean} isRescaled Flag for rescaled values (default false). * @param {Array} regions An array of regions. * @returns {object} The slice iterator. */ dwv.image.getVariableRegionSliceIterator = function ( - image, slice, frame, isRescaled, regions) { + image, position, isRescaled, regions) { if (image.getNumberOfComponents() !== 1) { throw new Error('Unsupported number of components for region iterator: ' + image.getNumberOfComponents()); } + // default to non rescaled data if (typeof isRescaled === 'undefined') { isRescaled = false; } - var geometry = image.getGeometry(); - var size = geometry.getSize(); + var dataAccessor = null; + if (isRescaled) { + dataAccessor = function (offset) { + return image.getRescaledValueAtOffset(offset); + }; + } else { + dataAccessor = function (offset) { + return image.getValueAtOffset(offset); + }; + } + + var size = image.getGeometry().getSize(); var offsetRegions = []; var region; @@ -327,7 +493,7 @@ dwv.image.getVariableRegionSliceIterator = function ( offsetRegions.push([ region[0][0], width, - size.getNumberOfColumns() - region[1][0] + size.get(0) - region[1][0] ]); } } @@ -340,22 +506,12 @@ dwv.image.getVariableRegionSliceIterator = function ( return; } - var minIndex = new dwv.math.Index3D(min[0], min[1], slice); - var startOffset = geometry.indexToOffset(minIndex); - var maxIndex = new dwv.math.Index3D(max[0], max[1], slice); - var endOffset = geometry.indexToOffset(maxIndex); - - // data accessor - var dataAccessor = null; - if (isRescaled) { - dataAccessor = function (offset) { - return image.getRescaledValueAtOffset(offset, slice, frame); - }; - } else { - dataAccessor = function (offset) { - return image.getValueAtOffset(offset, frame); - }; - } + var startOffset = size.indexToOffset(position.getWithNew2D( + min[0], min[1] + )); + var endOffset = size.indexToOffset(position.getWithNew2D( + max[0], max[1] + )); return dwv.image.rangeRegions( dataAccessor, startOffset, endOffset + 1, diff --git a/src/image/planeHelper.js b/src/image/planeHelper.js new file mode 100644 index 0000000000..b495f6c84f --- /dev/null +++ b/src/image/planeHelper.js @@ -0,0 +1,198 @@ +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Plane geometry helper. + * + * @class + * @param {dwv.image.Spacing} spacing The spacing. + * @param {dwv.math.Matrix} orientation The orientation. + */ +dwv.image.PlaneHelper = function (spacing, orientation) { + + /** + * Get a 3D offset from a plane one. + * + * @param {object} offset2D The plane offset as {x,y}. + * @returns {dwv.math.Vector3D} The 3D world offset. + */ + this.getOffset3DFromPlaneOffset = function (offset2D) { + // make 3D + var planeOffset = new dwv.math.Vector3D( + offset2D.x, offset2D.y, 0); + // de-orient + var pixelOffset = this.getDeOrientedVector3D(planeOffset); + // offset indexToWorld + return offsetIndexToWorld(pixelOffset); + }; + + /** + * Get a plane offset from a 3D one. + * + * @param {dwv.math.Point3D} offset3D The 3D offset. + * @returns {object} The plane offset as {x,y}. + */ + this.getPlaneOffsetFromOffset3D = function (offset3D) { + // offset worldToIndex + var pixelOffset = offsetWorldToIndex(offset3D); + // orient + var planeOffset = this.getOrientedVector3D(pixelOffset); + // make 2D + return { + x: planeOffset.getX(), + y: planeOffset.getY() + }; + }; + + /** + * Apply spacing to an offset. + * + * @param {dwv.math.Point3D} off The 3D offset. + * @returns {dwv.math.Vector3D} The world offset. + */ + function offsetIndexToWorld(off) { + return new dwv.math.Vector3D( + off.getX() * spacing.get(0), + off.getY() * spacing.get(1), + off.getZ() * spacing.get(2)); + } + + /** + * Remove spacing from an offset. + * + * @param {object} off The world offset object as {x,y,z}. + * @returns {dwv.math.Vector3D} The 3D offset. + */ + function offsetWorldToIndex(off) { + return new dwv.math.Vector3D( + off.x / spacing.get(0), + off.y / spacing.get(1), + off.z / spacing.get(2)); + } + + /** + * Orient an input vector. + * + * @param {dwv.math.Vector3D} vector The input vector. + * @returns {dwv.math.Vector3D} The oriented vector. + */ + this.getOrientedVector3D = function (vector) { + var planeVector = vector; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + planeVector = orientation.getInverse().getAbs().multiplyVector3D(vector); + } + return planeVector; + }; + + /** + * Orient an input index. + * + * @param {dwv.math.Index} index The input index. + * @returns {dwv.math.Index} The oriented index. + */ + this.getOrientedIndex = function (index) { + var planeIndex = index; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + planeIndex = orientation.getInverse().getAbs().multiplyIndex3D(index); + } + return planeIndex; + }; + + /** + * Orient an input point. + * + * @param {dwv.math.Point3D} point The input point. + * @returns {dwv.math.Point3D} The oriented point. + */ + this.getOrientedPoint = function (point) { + var planePoint = point; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planeVector + var point3D = + orientation.getInverse().getAbs().multiplyPoint3D(point.get3D()); + planePoint = point.mergeWith3D(point3D); + } + return planePoint; + }; + + /** + * De-orient an input vector. + * + * @param {dwv.math.Vector3D} planeVector The input vector. + * @returns {dwv.math.Vector3D} The de-orienteded vector. + */ + this.getDeOrientedVector3D = function (planeVector) { + var vector = planeVector; + if (typeof orientation !== 'undefined') { + // abs? otherwise negative index... + // vector = orientation * planePoint + vector = orientation.getAbs().multiplyVector3D(planeVector); + } + return vector; + }; + + /** + * Reorder values to follow orientation. + * + * @param {object} values Values as {x,y,z}. + * @returns {object} Reoriented values as {x,y,z}. + */ + this.getOrientedXYZ = function (values) { + var orientedValues = dwv.math.getOrientedArray3D( + [ + values.x, + values.y, + values.z + ], + orientation); + return { + x: orientedValues[0], + y: orientedValues[1], + z: orientedValues[2] + }; + }; + + /** + * Reorder values to compensate for orientation. + * + * @param {object} values Values as {x,y,z}. + * @returns {object} 'Deoriented' values as {x,y,z}. + */ + this.getDeOrientedXYZ = function (values) { + var deOrientedValues = dwv.math.getDeOrientedArray3D( + [ + values.x, + values.y, + values.z + ], + orientation + ); + return { + x: deOrientedValues[0], + y: deOrientedValues[1], + z: deOrientedValues[2] + }; + }; + + /** + * Get the scroll dimension index. + * + * @returns {number} The index. + */ + this.getScrollIndex = function () { + var index = null; + if (typeof orientation !== 'undefined') { + index = orientation.getThirdColMajorDirection(); + } else { + index = 2; + } + return index; + }; + +}; diff --git a/src/image/size.js b/src/image/size.js new file mode 100644 index 0000000000..e723e67a88 --- /dev/null +++ b/src/image/size.js @@ -0,0 +1,233 @@ +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Immutable Size class. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The size values. + */ +dwv.image.Size = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create size with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create size with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val) && val !== 0; + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create size with non number or zero values.'); + } + + /** + * Get the size value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the index. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the size. + * + * @returns {string} The Size as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this index. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Size class + +/** + * Check if a dimension exists and has more than one element. + * + * @param {number} dimension The dimension to check. + * @returns {boolean} True if the size is more than one. + */ +dwv.image.Size.prototype.moreThanOne = function (dimension) { + return this.length() >= dimension + 1 && this.get(dimension) !== 1; +}; + +/** + * Check if the third direction of an orientation matrix has a size + * of more than one. + * + * @param {dwv.math.Matrix33} viewOrientation The orientation matrix. + * @returns {boolean} True if scrollable. + */ +dwv.image.Size.prototype.canScroll = function (viewOrientation) { + var dimension = 2; + if (typeof viewOrientation !== 'undefined') { + dimension = viewOrientation.getThirdColMajorDirection(); + } + return this.moreThanOne(dimension); +}; + +/** + * Get the size of a given dimension. + * + * @param {number} dimension The dimension. + * @param {number} start Optional start dimension to start counting from. + * @returns {number} The size. + */ +dwv.image.Size.prototype.getDimSize = function (dimension, start) { + if (dimension > this.length()) { + return null; + } + if (typeof start === 'undefined') { + start = 0; + } else { + if (start < 0 || start > dimension) { + throw new Error('Invalid start value for getDimSize'); + } + } + var size = 1; + for (var i = start; i < dimension; ++i) { + size *= this.get(i); + } + return size; +}; + +/** + * Get the total size. + * + * @param {number} start Optional start dimension to base the offset on. + * @returns {number} The total size. + */ +dwv.image.Size.prototype.getTotalSize = function (start) { + return this.getDimSize(this.length(), start); +}; + +/** + * Check for equality. + * + * @param {dwv.image.Size} rhs The object to compare to. + * @returns {boolean} True if both objects are equal. + */ +dwv.image.Size.prototype.equals = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + var length = this.length(); + if (length !== rhs.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Check that an index is within bounds. + * + * @param {dwv.math.Index} index The index to check. + * @returns {boolean} True if the given coordinates are within bounds. + */ +dwv.image.Size.prototype.isInBounds = function (index) { + // check input + if (!index) { + return false; + } + // check length + var length = this.length(); + if (length !== index.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (index.get(i) < 0 || index.get(i) > this.get(i) - 1) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Convert an index to an offset in memory. + * + * @param {dwv.math.Index} index The index to convert. + * @param {number} start Optional start dimension to base the offset on. + * @returns {number} The offset. + */ +dwv.image.Size.prototype.indexToOffset = function (index, start) { + // TODO check for equality + if (index.length() < this.length()) { + throw new Error('Incompatible index and size length'); + } + if (typeof start === 'undefined') { + start = 0; + } else { + if (start < 0 || start > this.length() - 1) { + throw new Error('Invalid start value for indexToOffset'); + } + } + var offset = 0; + for (var i = start; i < this.length(); ++i) { + offset += index.get(i) * this.getDimSize(i, start); + } + return offset; +}; + +/** + * Convert an offset in memory to an index. + * + * @param {number} offset The offset to convert. + * @returns {dwv.math.Index} The index. + */ +dwv.image.Size.prototype.offsetToIndex = function (offset) { + var values = new Array(this.length()); + var off = offset; + var dimSize = 0; + for (var i = this.length() - 1; i > 0; --i) { + dimSize = this.getDimSize(i); + values[i] = Math.floor(off / dimSize); + off = off - values[i] * dimSize; + } + values[0] = off; + return new dwv.math.Index(values); +}; + +/** + * Get the 2D base of this size. + * + * @returns {object} The 2D base [0,1] as {x,y}. + */ +dwv.image.Size.prototype.get2D = function () { + return { + x: this.get(0), + y: this.get(1) + }; +}; diff --git a/src/image/spacing.js b/src/image/spacing.js new file mode 100644 index 0000000000..5db639c4e4 --- /dev/null +++ b/src/image/spacing.js @@ -0,0 +1,102 @@ +// namespaces +var dwv = dwv || {}; +dwv.image = dwv.image || {}; + +/** + * Immutable Spacing class. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The size values. + */ +dwv.image.Spacing = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create spacing with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create spacing with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val) && val !== 0; + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create spacing with non number or zero values.'); + } + + /** + * Get the spacing value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the spacing. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the spacing. + * + * @returns {string} The spacing as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this spacing. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Spacing class + +/** + * Check for equality. + * + * @param {dwv.image.Spacing} rhs The object to compare to. + * @returns {boolean} True if both objects are equal. + */ +dwv.image.Spacing.prototype.equals = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + var length = this.length(); + if (length !== rhs.length()) { + return false; + } + // check values + for (var i = 0; i < length; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Get the 2D base of this size. + * + * @returns {object} The 2D base [col,row] as {x,y}. + */ +dwv.image.Spacing.prototype.get2D = function () { + return { + x: this.get(0), + y: this.get(1) + }; +}; diff --git a/src/image/view.js b/src/image/view.js index c374d62b9a..6bea6cb8ca 100644 --- a/src/image/view.js +++ b/src/image/view.js @@ -8,13 +8,12 @@ dwv.image = dwv.image || {}; * @type {Array} */ dwv.image.viewEventNames = [ - 'slicechange', - 'framechange', - 'wlwidthchange', - 'wlcenterchange', + 'wlchange', 'wlpresetadd', 'colourchange', - 'positionchange' + 'positionchange', + 'opacitychange', + 'alphafuncchange' ]; /** @@ -26,6 +25,22 @@ dwv.image.viewEventNames = [ * (either directly or with helper methods). */ dwv.image.View = function (image) { + // closure to self + var self = this; + + // listen to appendframe event to update the current position + // to add the extra dimension + image.addEventListener('appendframe', function () { + // update current position if first appendFrame + var position = self.getCurrentPosition(); + if (position.length() === 3) { + // add dimension + var values = position.getValues(); + values.push(0); + self.setCurrentPosition(new dwv.math.Point(values)); + } + }); + /** * Window lookup tables, indexed per Rescale Slope and Intercept (RSI). * @@ -67,19 +82,27 @@ dwv.image.View = function (image) { */ var colourMap = dwv.image.lut.plain; /** - * Current position. + * Current position as a Point3D. * * @private * @type {object} */ var currentPosition = null; /** - * Current frame. Zero based. + * View orientation. Undefined will use the original slice ordering. + * + * @private + * @type {object} + */ + var orientation; + + /** + * Listener handler. * + * @type {object} * @private - * @type {number} */ - var currentFrame = null; + var listenerHandler = new dwv.utils.ListenerHandler(); /** * Get the associated image. @@ -98,13 +121,39 @@ dwv.image.View = function (image) { image = inImage; }; + /** + * Get the view orientation. + * + * @returns {dwv.math.Matrix33} The orientation matrix. + */ + this.getOrientation = function () { + return orientation; + }; + + /** + * Set the view orientation. + * + * @param {dwv.math.Matrix33} mat33 The orientation matrix. + */ + this.setOrientation = function (mat33) { + orientation = mat33; + }; + /** * Set initial position. */ this.setInitialPosition = function () { var silent = true; - this.setCurrentPosition({i: 0, j: 0, k: 0}, silent); - this.setCurrentFrame(0, silent); + + var geometry = image.getGeometry(); + var values = new Array(geometry.getSize().length()); + values.fill(0); + var index = new dwv.math.Index(values); + + this.setCurrentPosition( + geometry.indexToWorld(index), + silent + ); }; /** @@ -122,6 +171,46 @@ dwv.image.View = function (image) { return Math.round(1000 / recommendedDisplayFrameRate); }; + /** + * Per value alpha function. + * + * @param {*} _value The pixel value. Can be a number for monochrome + * data or an array for RGB data. + * @returns {number} The coresponding alpha [0,255]. + */ + var alphaFunction = function (_value) { + // default always returns fully visible + return 0xff; + }; + + /** + * Get the alpha function. + * + * @returns {Function} The function. + */ + this.getAlphaFunction = function () { + return alphaFunction; + }; + + /** + * Set alpha function. + * + * @param {Function} func The function. + * @fires dwv.image.View#alphafuncchange + */ + this.setAlphaFunction = function (func) { + alphaFunction = func; + /** + * Alpha func change event. + * + * @event dwv.image.View#alphafuncchange + * @type {object} + */ + fireEvent({ + type: 'alphafuncchange' + }); + }; + /** * Get the window LUT of the image. * Warning: can be undefined in no window/level was set. @@ -129,18 +218,17 @@ dwv.image.View = function (image) { * @param {object} rsi Optional image rsi, will take the one of the * current slice otherwise. * @returns {Window} The window LUT of the image. - * @fires dwv.image.View#wlwidthchange - * @fires dwv.image.View#wlcenterchange + * @fires dwv.image.View#wlchange */ this.getCurrentWindowLut = function (rsi) { - // check position (also sets frame) + // check position if (!this.getCurrentPosition()) { this.setInitialPosition(); } - var sliceNumber = this.getCurrentPosition().k; + var currentIndex = this.getCurrentIndex(); // use current rsi if not provided if (typeof rsi === 'undefined') { - rsi = image.getRescaleSlopeAndIntercept(sliceNumber); + rsi = image.getRescaleSlopeAndIntercept(currentIndex); } // get the current window level @@ -151,7 +239,8 @@ dwv.image.View = function (image) { typeof windowPresets[currentPresetName].perslice !== 'undefined' && windowPresets[currentPresetName].perslice === true) { // get the preset for this slice - wl = windowPresets[currentPresetName].wl[sliceNumber]; + var offset = image.getSecondaryOffset(currentIndex); + wl = windowPresets[currentPresetName].wl[offset]; } // regular case if (!wl) { @@ -183,19 +272,12 @@ dwv.image.View = function (image) { wlut.setWindowLevel(wl); wlut.update(); // fire change event - if (!lutWl || lutWl.getWidth() !== wl.getWidth()) { - this.fireEvent({ - type: 'wlwidthchange', - value: [wl.getWidth()], - wc: wl.getCenter(), - ww: wl.getWidth(), - skipGenerate: true - }); - } - if (!lutWl || lutWl.getCenter() !== wl.getCenter()) { - this.fireEvent({ - type: 'wlcenterchange', - value: [wl.getCenter()], + if (!lutWl || + lutWl.getWidth() !== wl.getWidth() || + lutWl.getCenter() !== wl.getCenter()) { + fireEvent({ + type: 'wlchange', + value: [wl.getCenter(), wl.getWidth()], wc: wl.getCenter(), ww: wl.getWidth(), skipGenerate: true @@ -256,18 +338,16 @@ dwv.image.View = function (image) { * Add window presets to the existing ones. * * @param {object} presets The window presets. - * @param {number} k The slice the preset belong to. */ - this.addWindowPresets = function (presets, k) { + this.addWindowPresets = function (presets) { var keys = Object.keys(presets); var key = null; for (var i = 0; i < keys.length; ++i) { key = keys[i]; if (typeof windowPresets[key] !== 'undefined') { if (typeof windowPresets[key].perslice !== 'undefined' && - windowPresets[key].perslice === true) { - // use first new preset wl... - windowPresets[key].wl.splice(k, 0, presets[key].wl[0]); + windowPresets[key].perslice === true) { + throw new Error('Cannot add perslice preset'); } else { windowPresets[key] = presets[key]; } @@ -282,7 +362,7 @@ dwv.image.View = function (image) { * @type {object} * @property {string} name The name of the preset. */ - this.fireEvent({ + fireEvent({ type: 'wlpresetadd', name: key }); @@ -315,7 +395,7 @@ dwv.image.View = function (image) { * @property {number} wc The new window center value. * @property {number} ww The new window wdth value. */ - this.fireEvent({ + fireEvent({ type: 'colourchange', wc: this.getCurrentWindowLut().getWindowLevel().getCenter(), ww: this.getCurrentWindowLut().getWindowLevel().getWidth() @@ -325,101 +405,102 @@ dwv.image.View = function (image) { /** * Get the current position. * - * @returns {object} The current position. + * @returns {dwv.math.Point} The current position. */ this.getCurrentPosition = function () { - // return a clone to avoid reference problems - return currentPosition ? { - i: currentPosition.i, - j: currentPosition.j, - k: currentPosition.k - } : null; + return currentPosition; }; + + /** + * Get the current index. + * + * @returns {dwv.math.Index} The current index. + */ + this.getCurrentIndex = function () { + var geometry = this.getImage().getGeometry(); + return geometry.worldToIndex(currentPosition); + }; + /** * Set the current position. * - * @param {object} pos The current position. - * @param {boolean} silent If true, does not fire a slicechange event. + * @param {dwv.math.Point} newPosition The new position. + * @param {boolean} silent Flag to fire event or not. * @returns {boolean} False if not in bounds - * @fires dwv.image.View#slicechange * @fires dwv.image.View#positionchange */ - this.setCurrentPosition = function (pos, silent) { + this.setCurrentPosition = function (newPosition, silent) { // check input if (typeof silent === 'undefined') { silent = false; } // check if possible - if (!image.getGeometry().getSize().isInBounds(pos.i, pos.j, pos.k)) { + var geometry = image.getGeometry(); + if (!geometry.isInBounds(newPosition)) { return false; } - // check if new - var equalPos = function (pos1, pos2) { - return pos2 !== null && - pos1.i === pos2.i && - pos1.j === pos2.j && - pos1.k === pos2.k; - }; - var isNew = !equalPos(pos, currentPosition); + + var isNew = !currentPosition || !currentPosition.equals(newPosition); if (isNew) { - var isNewSlice = currentPosition - ? pos.k !== currentPosition.k : true; + var posIndex = geometry.worldToIndex(newPosition); + var diffDims = null; + if (currentPosition) { + if (currentPosition.canCompare(newPosition)) { + diffDims = currentPosition.compare(newPosition); + } else { + diffDims = []; + var minLen = Math.min(currentPosition.length(), newPosition.length()); + for (var i = 0; i < minLen; ++i) { + if (currentPosition.get(i) !== newPosition.get(i)) { + diffDims.push(i); + } + } + var maxLen = Math.max(currentPosition.length(), newPosition.length()); + for (var j = minLen; j < maxLen; ++j) { + diffDims.push(j); + } + } + } else { + diffDims = []; + for (var k = 0; k < newPosition.length(); ++k) { + diffDims.push(k); + } + } + // assign - currentPosition = pos; + currentPosition = newPosition; - // fire a 'positionchange' event - if (image.getPhotometricInterpretation().match(/MONOCHROME/) !== null) { - var pixValue = image.getRescaledValue( - pos.i, pos.j, pos.k, this.getCurrentFrame()); + if (!silent) { /** * Position change event. * * @event dwv.image.View#positionchange * @type {object} - * @property {Array} value The changed value. - * @property {number} i The new column position - * @property {number} j The new row position - * @property {number} k The new slice position - * @property {object} pixelValue The image value at the new position, - * (can be undefined). + * @property {Array} value The changed value as [index, pixelValue]. + * @property {Array} diffDims An array of modified indices. */ - this.fireEvent({ + var posEvent = { type: 'positionchange', - value: [pos.i, pos.j, pos.k, pixValue], - i: pos.i, - j: pos.j, - k: pos.k, - pixelValue: pixValue - }); - } else { - this.fireEvent({ - type: 'positionchange', - value: [pos.i, pos.j, pos.k], - i: pos.i, - j: pos.j, - k: pos.k - }); - } - - // fire a slice change event (used to trigger redraw) - if (!silent && isNewSlice) { - /** - * Slice change event. - * - * @event dwv.image.View#slicechange - * @type {object} - * @property {Array} value The changed value. - * @property {object} data Associated event data: the imageUid. - */ - this.fireEvent({ - type: 'slicechange', - value: [currentPosition.k], + value: [ + posIndex.getValues(), + currentPosition.getValues(), + ], + diffDims: diffDims, data: { - imageUid: image.getImageUids()[currentPosition.k] + imageUid: image.getImageUid(posIndex) } - }); + }; + + // add value if possible + if (image.canQuantify()) { + var pixValue = image.getRescaledValueAtIndex(posIndex); + posEvent.value.push(pixValue); + } + + // fire + fireEvent(posEvent); } } @@ -428,59 +509,15 @@ dwv.image.View = function (image) { }; /** - * Get the current frame number. + * Set the current index. * - * @returns {number} The current frame number. + * @param {dwv.math.Index} index The index. + * @param {boolean} silent If true, does not fire a positionchange event. + * @returns {boolean} False if not in bounds. */ - this.getCurrentFrame = function () { - return currentFrame; - }; - - /** - * Set the current frame number. - * - * @param {number} frame The current frame number. - * @param {boolean} silent Flag to launch events with skipGenerate. - * @returns {boolean} False if not in bounds - * @fires dwv.image.View#framechange - */ - this.setCurrentFrame = function (frame, silent) { - // check input - if (typeof silent === 'undefined') { - silent = false; - } - - // check if possible - if (frame < 0 || frame >= image.getNumberOfFrames()) { - return false; - } - // check if new - var isNew = currentFrame !== frame; - - if (isNew) { - // assign - currentFrame = frame; - // fire event for multi frame data - if (image.getNumberOfFrames() !== 1) { - /** - * Frame change event. - * - * @event dwv.image.View#framechange - * @type {object} - * @property {Array} value The changed value. - * @property {number} frame The new frame number - * @property {boolean} skipGenerate Flag to skip view generation. - */ - this.fireEvent({ - type: 'framechange', - value: [currentFrame], - frame: currentFrame, - skipGenerate: silent - }); - } - } - // all good - return true; + this.setCurrentIndex = function (index, silent) { + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(index), silent); }; /** @@ -491,8 +528,7 @@ dwv.image.View = function (image) { * @param {string} name Associated preset name, defaults to 'manual'. * Warning: uses the latest set rescale LUT or the default linear one. * @param {boolean} silent Flag to launch events with skipGenerate. - * @fires dwv.image.View#wlwidthchange - * @fires dwv.image.View#wlcenterchange + * @fires dwv.image.View#wlchange */ this.setWindowLevel = function (center, width, name, silent) { // window width shall be >= 1 (see https://www.dabsoft.ch/dicom/3/C.11.2.1.2/) @@ -522,40 +558,20 @@ dwv.image.View = function (image) { currentWl = newWl; currentPresetName = name; - if (isNewWidth) { + if (isNewWidth || isNewCenter) { /** - * Window/level width change event. + * Window/level change event. * - * @event dwv.image.View#wlwidthchange + * @event dwv.image.View#wlchange * @type {object} * @property {Array} value The changed value. * @property {number} wc The new window center value. * @property {number} ww The new window wdth value. * @property {boolean} skipGenerate Flag to skip view generation. */ - this.fireEvent({ - type: 'wlwidthchange', - value: [width], - wc: center, - ww: width, - skipGenerate: silent - }); - } - - if (isNewCenter) { - /** - * Window/level center change event. - * - * @event dwv.image.View#wlcenterchange - * @type {object} - * @property {Array} value The changed value. - * @property {number} wc The new window center value. - * @property {number} ww The new window wdth value. - * @property {boolean} skipGenerate Flag to skip view generation. - */ - this.fireEvent({ - type: 'wlcenterchange', - value: [center], + fireEvent({ + type: 'wlchange', + value: [center, width], wc: center, ww: width, skipGenerate: silent @@ -577,16 +593,19 @@ dwv.image.View = function (image) { } // special min/max if (name === 'minmax' && typeof preset.wl === 'undefined') { - preset.wl = this.getWindowLevelMinMax(); + preset.wl = [this.getWindowLevelMinMax()]; } - // special 'perslice' case + // default to first + var wl = preset.wl[0]; + // check if 'perslice' case if (typeof preset.perslice !== 'undefined' && preset.perslice === true) { - preset = {wl: preset.wl[this.getCurrentPosition().k]}; + var offset = image.getSecondaryOffset(this.getCurrentIndex()); + wl = preset.wl[offset]; } // set w/l this.setWindowLevel( - preset.wl.getCenter(), preset.wl.getWidth(), name, silent); + wl.getCenter(), wl.getWidth(), name, silent); }; /** @@ -615,28 +634,36 @@ dwv.image.View = function (image) { }; /** - * View listeners + * Add an event listener to this class. * - * @private - * @type {object} + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type, will be called with the fired event. */ - var listeners = {}; + this.addEventListener = function (type, callback) { + listenerHandler.add(type, callback); + }; + /** - * Get the view listeners. + * Remove an event listener from this class. * - * @returns {object} The view listeners. + * @param {string} type The event type. + * @param {object} callback The method associated with the provided + * event type. */ - this.getListeners = function () { - return listeners; + this.removeEventListener = function (type, callback) { + listenerHandler.remove(type, callback); }; + /** - * Set the view listeners. + * Fire an event: call all associated listeners with the input event object. * - * @param {object} list The view listeners. + * @param {object} event The event to fire. + * @private */ - this.setListeners = function (list) { - listeners = list; - }; + function fireEvent(event) { + listenerHandler.fireEvent(event); + } }; /** @@ -676,14 +703,14 @@ dwv.image.View.prototype.setWindowLevelMinMax = function () { * @param {Array} array The array to fill in. */ dwv.image.View.prototype.generateImageData = function (array) { - // check position (also sets frame) + // check position if (!this.getCurrentPosition()) { this.setInitialPosition(); } - var position = this.getCurrentPosition(); - var frame = this.getCurrentFrame(); var image = this.getImage(); - var iterator = dwv.image.getSliceIterator(this.getImage(), position.k, frame); + var position = this.getCurrentIndex(); + var iterator = dwv.image.getSliceIterator( + image, position, false, this.getOrientation()); var photoInterpretation = image.getPhotometricInterpretation(); switch (photoInterpretation) { @@ -692,6 +719,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataMonochrome( array, iterator, + this.getAlphaFunction(), this.getCurrentWindowLut(), this.getColourMap() ); @@ -701,6 +729,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataPaletteColor( array, iterator, + this.getAlphaFunction(), this.getColourMap(), image.getMeta().BitsStored === 16 ); @@ -710,6 +739,7 @@ dwv.image.View.prototype.generateImageData = function (array) { dwv.image.generateImageDataRgb( array, iterator, + this.getAlphaFunction(), this.getCurrentWindowLut() ); break; @@ -717,7 +747,8 @@ dwv.image.View.prototype.generateImageData = function (array) { case 'YBR_FULL': dwv.image.generateImageDataYbrFull( array, - iterator + iterator, + this.getAlphaFunction() ); break; @@ -728,48 +759,81 @@ dwv.image.View.prototype.generateImageData = function (array) { }; /** - * Add an event listener on the view. + * Increment the provided dimension. * - * @param {string} type The event type. - * @param {object} listener The method associated with the provided event type. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. */ -dwv.image.View.prototype.addEventListener = function (type, listener) { - var listeners = this.getListeners(); - if (!listeners[type]) { - listeners[type] = []; +dwv.image.View.prototype.incrementIndex = function (dim, silent) { + var index = this.getCurrentIndex(); + var values = new Array(index.length()); + values.fill(0); + if (dim < values.length) { + values[dim] = 1; + } else { + console.warn('Cannot increment given index: ', dim, values.length); } - listeners[type].push(listener); + var incr = new dwv.math.Index(values); + var newIndex = index.add(incr); + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent); }; /** - * Remove an event listener on the view. + * Decrement the provided dimension. * - * @param {string} type The event type. - * @param {object} listener The method associated with the provided event type. + * @param {number} dim The dimension to increment. + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. */ -dwv.image.View.prototype.removeEventListener = function (type, listener) { - var listeners = this.getListeners(); - if (!listeners[type]) { - return; - } - for (var i = 0; i < listeners[type].length; ++i) { - if (listeners[type][i] === listener) { - listeners[type].splice(i, 1); - } +dwv.image.View.prototype.decrementIndex = function (dim, silent) { + var index = this.getCurrentIndex(); + var values = new Array(index.length()); + values.fill(0); + if (dim < values.length) { + values[dim] = -1; + } else { + console.warn('Cannot decrement given index: ', dim, values.length); } + var incr = new dwv.math.Index(values); + var newIndex = index.add(incr); + var geometry = this.getImage().getGeometry(); + return this.setCurrentPosition(geometry.indexToWorld(newIndex), silent); }; /** - * Fire an event: call all associated listeners. + * Get the scroll dimension index. * - * @param {object} event The event to fire. + * @returns {number} The index. */ -dwv.image.View.prototype.fireEvent = function (event) { - var listeners = this.getListeners(); - if (!listeners[event.type]) { - return; - } - for (var i = 0; i < listeners[event.type].length; ++i) { - listeners[event.type][i](event); +dwv.image.View.prototype.getScrollIndex = function () { + var index = null; + var orientation = this.getOrientation(); + if (typeof orientation !== 'undefined') { + index = orientation.getThirdColMajorDirection(); + } else { + index = 2; } + return index; +}; + +/** + * Decrement the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ +dwv.image.View.prototype.decrementScrollIndex = function (silent) { + return this.decrementIndex(this.getScrollIndex(), silent); +}; + +/** + * Increment the scroll dimension index. + * + * @param {boolean} silent Do not send event. + * @returns {boolean} False if not in bounds. + */ +dwv.image.View.prototype.incrementScrollIndex = function (silent) { + return this.incrementIndex(this.getScrollIndex(), silent); }; diff --git a/src/image/viewFactory.js b/src/image/viewFactory.js index 6c2cb64674..1390e79649 100644 --- a/src/image/viewFactory.js +++ b/src/image/viewFactory.js @@ -3,17 +3,24 @@ var dwv = dwv || {}; dwv.image = dwv.image || {}; /** - * View factory. + * {@link dwv.image.View} factory. * * @class */ dwv.image.ViewFactory = function () {}; +/** + * {@link dwv.image.View} factory. Defaults to local one. + * + * @see dwv.image.ViewFactory + */ +dwv.ViewFactory = dwv.image.ViewFactory; + /** * Get an View object from the read DICOM file. * * @param {object} dicomElements The DICOM tags. - * @param {object} image The associated image. + * @param {dwv.image.Image} image The associated image. * @returns {dwv.image.View} The new View. */ dwv.image.ViewFactory.prototype.create = function (dicomElements, image) { diff --git a/src/image/viewMonochrome.js b/src/image/viewMonochrome.js index 86449a7fd4..e091465e68 100644 --- a/src/image/viewMonochrome.js +++ b/src/image/viewMonochrome.js @@ -7,12 +7,14 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. * @param {object} windowLut The window/level LUT. * @param {object} colourMap The colour map. */ dwv.image.generateImageDataMonochrome = function ( array, iterator, + alphaFunc, windowLut, colourMap) { var index = 0; @@ -25,7 +27,7 @@ dwv.image.generateImageDataMonochrome = function ( array.data[index] = colourMap.red[pxValue]; array.data[index + 1] = colourMap.green[pxValue]; array.data[index + 2] = colourMap.blue[pxValue]; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); diff --git a/src/image/viewPaletteColor.js b/src/image/viewPaletteColor.js index 04ebfe6f5b..8281cafc58 100644 --- a/src/image/viewPaletteColor.js +++ b/src/image/viewPaletteColor.js @@ -7,12 +7,14 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. * @param {object} colourMap The colour map. * @param {boolean} is16BitsStored Flag to know if the data is 16bits. */ dwv.image.generateImageDataPaletteColor = function ( array, iterator, + alphaFunc, colourMap, is16BitsStored) { // right shift 8 @@ -41,7 +43,7 @@ dwv.image.generateImageDataPaletteColor = function ( array.data[index + 1] = colourMap.green[pxValue]; array.data[index + 2] = colourMap.blue[pxValue]; } - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(pxValue); // increment index += 4; ival = iterator.next(); diff --git a/src/image/viewRgb.js b/src/image/viewRgb.js index e5753d0b0f..1e1cd6765e 100644 --- a/src/image/viewRgb.js +++ b/src/image/viewRgb.js @@ -7,10 +7,12 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. */ dwv.image.generateImageDataRgb = function ( array, - iterator) { + iterator, + alphaFunc) { var index = 0; var ival = iterator.next(); while (!ival.done) { @@ -18,7 +20,7 @@ dwv.image.generateImageDataRgb = function ( array.data[index] = ival.value[0]; array.data[index + 1] = ival.value[1]; array.data[index + 2] = ival.value[2]; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); diff --git a/src/image/viewYbrFull.js b/src/image/viewYbrFull.js index de75cc0749..9ec2c8b26d 100644 --- a/src/image/viewYbrFull.js +++ b/src/image/viewYbrFull.js @@ -7,10 +7,12 @@ dwv.image = dwv.image || {}; * * @param {Array} array The array to store the outut data * @param {object} iterator Position iterator. + * @param {Function} alphaFunc The alpha function. */ dwv.image.generateImageDataYbrFull = function ( array, - iterator) { + iterator, + alphaFunc) { var index = 0; var rgb = null; var ival = iterator.next(); @@ -22,7 +24,7 @@ dwv.image.generateImageDataYbrFull = function ( array.data[index] = rgb.r; array.data[index + 1] = rgb.g; array.data[index + 2] = rgb.b; - array.data[index + 3] = 0xff; + array.data[index + 3] = alphaFunc(ival.value); // increment index += 4; ival = iterator.next(); diff --git a/src/io/dicomDataLoader.js b/src/io/dicomDataLoader.js index fadd7e731b..df1cb08f55 100644 --- a/src/io/dicomDataLoader.js +++ b/src/io/dicomDataLoader.js @@ -62,10 +62,8 @@ dwv.io.DicomDataLoader = function () { this.load = function (buffer, origin, index) { // setup db2v ony once if (!isLoading) { - // set character set - if (typeof options.defaultCharacterSet !== 'undefined') { - db2v.setDefaultCharacterSet(options.defaultCharacterSet); - } + // pass options + db2v.setOptions(options); // connect handlers db2v.onloadstart = self.onloadstart; db2v.onprogress = self.onprogress; diff --git a/src/io/filesLoader.js b/src/io/filesLoader.js index bf515ec329..6efd93c622 100644 --- a/src/io/filesLoader.js +++ b/src/io/filesLoader.js @@ -246,6 +246,7 @@ dwv.io.FilesLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: this.getDefaultCharacterSet() }); // set loader callbacks @@ -269,7 +270,7 @@ dwv.io.FilesLoader = function () { } } if (!foundLoader) { - throw new Error('No loader found for file: ' + dataElement); + throw new Error('No loader found for file: ' + dataElement.name); } var getLoadHandler = function (loader, dataElement, i) { diff --git a/src/io/memoryLoader.js b/src/io/memoryLoader.js index c4dc6556a8..6b1d028ad4 100644 --- a/src/io/memoryLoader.js +++ b/src/io/memoryLoader.js @@ -195,6 +195,7 @@ dwv.io.MemoryLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: this.getDefaultCharacterSet() }); // set loader callbacks diff --git a/src/app/state.js b/src/io/state.js similarity index 75% rename from src/app/state.js rename to src/io/state.js index e4d75f7e7f..04de2a0c4f 100644 --- a/src/app/state.js +++ b/src/io/state.js @@ -1,5 +1,6 @@ // namespaces var dwv = dwv || {}; +dwv.io = dwv.io || {}; // external var Konva = Konva || {}; @@ -8,6 +9,9 @@ var Konva = Konva || {}; * Saves: data url/path, display info. * * History: + * - v0.5 (dwv 0.30.0, ??/2021) + * - store position as array + * - new draw position group key * - v0.4 (dwv 0.29.0, 06/2021) * - move drawing details into meta property * - remove scale center and translation, add offset @@ -27,7 +31,7 @@ var Konva = Konva || {}; * * @class */ -dwv.State = function () { +dwv.io.State = function () { /** * Save the application state as JSON. * @@ -35,16 +39,17 @@ dwv.State = function () { * @returns {string} The state as a JSON string. */ this.toJSON = function (app) { - var layerController = app.getLayerController(); + var layerGroup = app.getActiveLayerGroup(); var viewController = - layerController.getActiveViewLayer().getViewController(); - var drawLayer = layerController.getActiveDrawLayer(); + layerGroup.getActiveViewLayer().getViewController(); + var drawLayer = layerGroup.getActiveDrawLayer(); + var position = viewController.getCurrentPosition(); // return a JSON string return JSON.stringify({ - version: '0.4', + version: '0.5', 'window-center': viewController.getWindowLevel().center, 'window-width': viewController.getWindowLevel().width, - position: viewController.getCurrentPosition(), + position: [position.getX(), position.getY(), position.getZ()], scale: app.getAddedScale(), offset: app.getOffset(), drawings: drawLayer.getKonvaLayer().toObject(), @@ -68,6 +73,8 @@ dwv.State = function () { res = readV03(data); } else if (data.version === '0.4') { res = readV04(data); + } else if (data.version === '0.5') { + res = readV05(data); } else { throw new Error('Unknown state file format version: \'' + data.version + '\'.'); @@ -81,21 +88,24 @@ dwv.State = function () { * @param {object} data The state data. */ this.apply = function (app, data) { - var layerController = app.getLayerController(); + var layerGroup = app.getActiveLayerGroup(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); // display viewController.setWindowLevel( data['window-center'], data['window-width']); - viewController.setCurrentPosition(data.position); + viewController.setCurrentPosition( + new dwv.math.Point3D( + data.position[0], data.position[1], data.position[2]), true); // apply saved scale on top of current base one - var baseScale = app.getLayerController().getBaseScale(); + var baseScale = app.getActiveLayerGroup().getBaseScale(); var scale = null; var offset = null; if (typeof data.scaleCenter !== 'undefined') { scale = { x: data.scale * baseScale.x, y: data.scale * baseScale.y, + z: 1 }; // ---- transform translation (now) ---- // Tx = -offset.x * scale.x @@ -110,19 +120,25 @@ dwv.State = function () { var oldTy = originY + data.translation.y * scale.y; offset = { x: -oldTx / scale.x, - y: -oldTy / scale.y + y: -oldTy / scale.y, + z: 0 }; } else { scale = { x: data.scale.x * baseScale.x, - y: data.scale.y * baseScale.y + y: data.scale.y * baseScale.y, + z: 1 + }; + offset = { + x: data.offset.x, + y: data.offset.y, + z: 0 }; - offset = data.offset; } - app.getLayerController().setScale(scale); - app.getLayerController().setOffset(offset); + app.getActiveLayerGroup().setScale(scale); + app.getActiveLayerGroup().setOffset(offset); // render to draw the view layer - app.render(); + app.render(0); //todo: fix // drawings (will draw the draw layer) app.setDrawings(data.drawings, data.drawingsDetails); }; @@ -134,11 +150,15 @@ dwv.State = function () { * @private */ function readV01(data) { - // update drawings - var v02DAndD = dwv.v01Tov02DrawingsAndDetails(data.drawings); - data.drawings = dwv.v02Tov03Drawings(v02DAndD.drawings).toObject(); - data.drawingsDetails = dwv.v03Tov04DrawingsDetails( + // v0.1 -> v0.2 + var v02DAndD = dwv.io.v01Tov02DrawingsAndDetails(data.drawings); + // v0.2 -> v0.3, v0.4 + data.drawings = dwv.io.v02Tov03Drawings(v02DAndD.drawings).toObject(); + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails( v02DAndD.drawingsDetails); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); return data; } /** @@ -149,10 +169,13 @@ dwv.State = function () { * @private */ function readV02(data) { - // update drawings - data.drawings = dwv.v02Tov03Drawings(data.drawings).toObject(); - data.drawingsDetails = dwv.v03Tov04DrawingsDetails( - dwv.v02Tov03DrawingsDetails(data.drawingsDetails)); + // v0.2 -> v0.3, v0.4 + data.drawings = dwv.io.v02Tov03Drawings(data.drawings).toObject(); + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails( + dwv.io.v02Tov03DrawingsDetails(data.drawingsDetails)); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); return data; } /** @@ -163,7 +186,11 @@ dwv.State = function () { * @private */ function readV03(data) { - data.drawingsDetails = dwv.v03Tov04DrawingsDetails(data.drawingsDetails); + // v0.3 -> v0.4 + data.drawingsDetails = dwv.io.v03Tov04DrawingsDetails(data.drawingsDetails); + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); return data; } /** @@ -174,6 +201,19 @@ dwv.State = function () { * @private */ function readV04(data) { + // v0.4 -> v0.5 + data = dwv.io.v04Tov05Data(data); + data.drawings = dwv.io.v04Tov05Drawings(data.drawings); + return data; + } + /** + * Read an application state from an Object in v0.5 format. + * + * @param {object} data The Object representation of the state. + * @returns {object} The state object. + * @private + */ + function readV05(data) { return data; } @@ -187,7 +227,7 @@ dwv.State = function () { * @param {Array} drawings An array of drawings. * @returns {object} The layer with the converted drawings. */ -dwv.v02Tov03Drawings = function (drawings) { +dwv.io.v02Tov03Drawings = function (drawings) { // Auxiliar variables var group, groupShapes, parentGroup; // Avoid errors when dropping multiple states @@ -211,7 +251,7 @@ dwv.v02Tov03Drawings = function (drawings) { if (groupShapes.length !== 0) { // Create position-group set as visible and append it to drawLayer parentGroup = new Konva.Group({ - id: dwv.draw.getDrawPositionGroupId(k, f), + id: dwv.draw.getDrawPositionGroupId(new dwv.math.Index([1, 1, k, f])), name: 'position-group', visible: false }); @@ -246,7 +286,7 @@ dwv.v02Tov03Drawings = function (drawings) { * @param {Array} inputDrawings An array of drawings. * @returns {object} The converted drawings. */ -dwv.v01Tov02DrawingsAndDetails = function (inputDrawings) { +dwv.io.v01Tov02DrawingsAndDetails = function (inputDrawings) { var newDrawings = []; var drawingsDetails = {}; @@ -272,7 +312,7 @@ dwv.v01Tov02DrawingsAndDetails = function (inputDrawings) { var kshape = drawGroup.getChildren(function (node) { return node.name() === 'shape'; })[0]; - kshape.stroke(dwv.getColourHex(kshape.stroke())); + kshape.stroke(dwv.utils.colourNameToHex(kshape.stroke())); // special line case if (drawGroup.name() === 'line-group') { // update name @@ -393,7 +433,7 @@ dwv.v01Tov02DrawingsAndDetails = function (inputDrawings) { * @param {Array} details An array of drawing details. * @returns {object} The converted drawings. */ -dwv.v02Tov03DrawingsDetails = function (details) { +dwv.io.v02Tov03DrawingsDetails = function (details) { var res = {}; // Get the positions-groups data var groupDetails = typeof details === 'string' @@ -424,7 +464,7 @@ dwv.v02Tov03DrawingsDetails = function (details) { * @param {Array} details An array of drawing details. * @returns {object} The converted drawings. */ -dwv.v03Tov04DrawingsDetails = function (details) { +dwv.io.v03Tov04DrawingsDetails = function (details) { var res = {}; var keys = Object.keys(details); // Iterate over each position-groups @@ -442,26 +482,43 @@ dwv.v03Tov04DrawingsDetails = function (details) { }; /** - * Get the hex code of a string colour for a colour used in pre dwv v0.17. + * Convert drawing from v0.4 to v0.5. + * - v0.4: position as object + * - v0.5: position as array + * + * @param {Array} data An array of drawing. + * @returns {object} The converted drawings. + */ +dwv.io.v04Tov05Data = function (data) { + var pos = data.position; + data.position = [pos.i, pos.j, pos.k]; + return data; +}; + +/** + * Convert drawing from v0.4 to v0.5. + * - v0.4: draw id as 'slice-0_frame-1' + * - v0.5: draw id as '#2-0_#3-1'' * - * @param {string} name The name of a colour. - * @returns {string} The hex representing the colour. + * @param {Array} inputDrawings An array of drawing. + * @returns {object} The converted drawings. */ -dwv.getColourHex = function (name) { - // default colours used in dwv version < 0.17 - var dict = { - Yellow: '#ffff00', - Red: '#ff0000', - White: '#ffffff', - Green: '#008000', - Blue: '#0000ff', - Lime: '#00ff00', - Fuchsia: '#ff00ff', - Black: '#000000' - }; - var res = '#ffff00'; - if (typeof dict[name] !== 'undefined') { - res = dict[name]; +dwv.io.v04Tov05Drawings = function (inputDrawings) { + // Iterate over each position-groups + var posGroups = inputDrawings.children; + for (var k = 0, lenk = posGroups.length; k < lenk; ++k) { + var posGroup = posGroups[k]; + var id = posGroup.attrs.id; + var ids = id.split('_'); + var sliceNumber = parseInt(ids[0].substring(6), 10); // 'slice-0' + var frameNumber = parseInt(ids[1].substring(6), 10); // 'frame-0' + var newId = '#2-'; + if (sliceNumber === 0 && frameNumber !== 0) { + newId += frameNumber; + } else { + newId += sliceNumber; + } + posGroup.attrs.id = newId; } - return res; + return inputDrawings; }; diff --git a/src/io/urlsLoader.js b/src/io/urlsLoader.js index 83660eaada..c5b8bd6af7 100644 --- a/src/io/urlsLoader.js +++ b/src/io/urlsLoader.js @@ -277,6 +277,7 @@ dwv.io.UrlsLoader = function () { foundLoader = true; // load options loader.setOptions({ + numberOfFiles: data.length, defaultCharacterSet: self.getDefaultCharacterSet() }); // set loader callbacks diff --git a/src/math/bucketQueue.js b/src/math/bucketQueue.js index 91fec63ac8..05a3f51a7b 100644 --- a/src/math/bucketQueue.js +++ b/src/math/bucketQueue.js @@ -60,6 +60,7 @@ dwv.math.BucketQueue.prototype.pop = function () { return ret; }; +// TODO: needs at least two items... dwv.math.BucketQueue.prototype.remove = function (item) { // Tries to remove item from queue. Returns true on success, false otherwise if (!item) { @@ -70,7 +71,10 @@ dwv.math.BucketQueue.prototype.remove = function (item) { var bucket = this.getBucket(item); var node = this.buckets[bucket]; - while (node !== null && !item.equals(node.next)) { + while (node !== null && + !(node.next !== null && + item.x === node.next.x && + item.y === node.next.y)) { node = node.next; } diff --git a/src/math/circle.js b/src/math/circle.js index 933acab987..e2375f45b7 100644 --- a/src/math/circle.js +++ b/src/math/circle.js @@ -23,14 +23,15 @@ dwv.math.mulABC = function (a, b, c) { * Circle shape. * * @class - * @param {object} centre A Point2D representing the centre of the circle. + * @param {dwv.math.Point2D} centre A Point2D representing the centre + * of the circle. * @param {number} radius The radius of the circle. */ dwv.math.Circle = function (centre, radius) { /** * Get the centre (point) of the circle. * - * @returns {object} The center (point) of the circle. + * @returns {dwv.math.Point2D} The center (point) of the circle. */ this.getCenter = function () { return centre; @@ -50,7 +51,7 @@ dwv.math.Circle = function (centre, radius) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Circle} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Circle.prototype.equals = function (rhs) { @@ -120,7 +121,8 @@ dwv.math.Circle.prototype.getRound = function () { /** * Quantify an circle according to view information. * - * @param {object} viewController The associated view controller. + * @param {dwv.ctrl.ViewController} viewController The associated view + * controller. * @param {Array} flags A list of stat values to calculate. * @returns {object} A quantification object. */ diff --git a/src/math/ellipse.js b/src/math/ellipse.js index dbb1438fa9..53dd91bba1 100644 --- a/src/math/ellipse.js +++ b/src/math/ellipse.js @@ -23,7 +23,8 @@ dwv.math.mulABC = function (a, b, c) { * Ellipse shape. * * @class - * @param {object} centre A Point2D representing the centre of the ellipse. + * @param {dwv.math.Point2D} centre A Point2D representing the centre + * of the ellipse. * @param {number} a The radius of the ellipse on the horizontal axe. * @param {number} b The radius of the ellipse on the vertical axe. */ @@ -31,7 +32,7 @@ dwv.math.Ellipse = function (centre, a, b) { /** * Get the centre (point) of the ellipse. * - * @returns {object} The center (point) of the ellipse. + * @returns {dwv.math.Point2D} The center (point) of the ellipse. */ this.getCenter = function () { return centre; @@ -59,7 +60,7 @@ dwv.math.Ellipse = function (centre, a, b) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Ellipse} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Ellipse.prototype.equals = function (rhs) { @@ -132,7 +133,8 @@ dwv.math.Ellipse.prototype.getRound = function () { /** * Quantify an ellipse according to view information. * - * @param {object} viewController The associated view controller. + * @param {dwv.ctrl.ViewController} viewController The associated view + * controller. * @param {Array} flags A list of stat values to calculate. * @returns {object} A quantification object. */ diff --git a/src/math/index.js b/src/math/index.js new file mode 100644 index 0000000000..93f57a463a --- /dev/null +++ b/src/math/index.js @@ -0,0 +1,213 @@ +// namespaces +var dwv = dwv || {}; +dwv.math = dwv.math || {}; + +/** + * Immutable index. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. + * + * @class + * @param {Array} values The index values. + */ +dwv.math.Index = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create index with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create index with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val); + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create index with non number values.'); + } + + /** + * Get the index value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + + /** + * Get the length of the index. + * + * @returns {number} The length. + */ + this.length = function () { + return values.length; + }; + + /** + * Get a string representation of the Index. + * + * @returns {string} The Index as a string. + */ + this.toString = function () { + return '(' + values.toString() + ')'; + }; + + /** + * Get the values of this index. + * + * @returns {Array} The array of values. + */ + this.getValues = function () { + return values.slice(); + }; + +}; // Index class + +/** + * Check if the input index can be compared to this one. + * + * @param {dwv.math.Index} rhs The index to compare to. + * @returns {boolean} True if both indices are comparable. + */ +dwv.math.Index.prototype.canCompare = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + if (this.length() !== rhs.length()) { + return false; + } + // seems ok! + return true; +}; + +/** + * Check for Index equality. + * + * @param {dwv.math.Index} rhs The index to compare to. + * @returns {boolean} True if both indices are equal. + */ +dwv.math.Index.prototype.equals = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return false; + } + // check values + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Add another index to this one. + * + * @param {dwv.math.Index} rhs The index to add. + * @returns {dwv.math.Index} The index representing the sum of both indices. + */ +dwv.math.Index.prototype.add = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + // add values + var values = []; + for (var i = 0, leni = this.length(); i < leni; ++i) { + values.push(this.get(i) + rhs.get(i)); + } + // seems ok! + return new dwv.math.Index(values); +}; + +/** + * Get the current index with a new 2D base. + * + * @param {number} i The new 0 index. + * @param {number} j The new 1 index. + * @returns {dwv.math.Index} The new index. + */ +dwv.math.Index.prototype.getWithNew2D = function (i, j) { + var values = [i, j]; + for (var l = 2, lenl = this.length(); l < lenl; ++l) { + values.push(this.get(l)); + } + return new dwv.math.Index(values); +}; + +/** + * Get an index with values set to 0 and the input size. + * + * @param {number} size The size of the index. + * @returns {dwv.math.Index} The zero index. + */ +dwv.math.getZeroIndex = function (size) { + var values = new Array(size); + values.fill(0); + return new dwv.math.Index(values); +}; + +/** + * Get a string id from the index values in the form of: '#0-1_#1-2'. + * + * @param {Array} dims Optional list of dimensions to use. + * @returns {string} The string id. + */ +dwv.math.Index.prototype.toStringId = function (dims) { + if (typeof dims === 'undefined') { + dims = []; + for (var j = 0; j < this.length(); ++j) { + dims.push(j); + } + } + for (var ii = 0; ii < dims.length; ++ii) { + if (dims[ii] >= this.length()) { + throw new Error('Non valid dimension for toStringId.'); + } + } + var res = ''; + for (var i = 0; i < dims.length; ++i) { + if (i !== 0) { + res += '_'; + } + res += '#' + dims[i] + '-' + this.get(dims[i]); + } + return res; +}; + +/** + * Get an index from an id string in the form of: '#0-1_#1-2' + * (result of index.toStringId). + * + * @param {string} inputStr The input string. + * @returns {dwv.math.Index} The corresponding index. + */ +dwv.math.getIndexFromStringId = function (inputStr) { + // split ids + var strIds = inputStr.split('_'); + // get the size of the index + var pointLength = 0; + var dim; + for (var i = 0; i < strIds.length; ++i) { + dim = parseInt(strIds[i].substring(1, 2), 10); + if (dim > pointLength) { + pointLength = dim; + } + } + if (pointLength === 0) { + throw new Error('No dimension found in point stringId'); + } + // default values + var values = new Array(pointLength); + values.fill(0); + // get other values from the input string + for (var j = 0; j < strIds.length; ++j) { + dim = parseInt(strIds[j].substring(1, 3), 10); + var value = parseInt(strIds[j].substring(3), 10); + values[dim] = value; + } + return new dwv.math.Point(values); +}; diff --git a/src/math/line.js b/src/math/line.js index 79882d8596..3bd7b73dcb 100644 --- a/src/math/line.js +++ b/src/math/line.js @@ -6,14 +6,15 @@ dwv.math = dwv.math || {}; * Line shape. * * @class - * @param {object} begin A Point2D representing the beginning of the line. - * @param {object} end A Point2D representing the end of the line. + * @param {dwv.math.Point2D} begin A Point2D representing the beginning + * of the line. + * @param {dwv.math.Point2D} end A Point2D representing the end of the line. */ dwv.math.Line = function (begin, end) { /** * Get the begin point of the line. * - * @returns {object} The beginning point of the line. + * @returns {dwv.math.Point2D} The beginning point of the line. */ this.getBegin = function () { return begin; @@ -22,7 +23,7 @@ dwv.math.Line = function (begin, end) { /** * Get the end point of the line. * - * @returns {object} The ending point of the line. + * @returns {dwv.math.Point2D} The ending point of the line. */ this.getEnd = function () { return end; @@ -32,7 +33,7 @@ dwv.math.Line = function (begin, end) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Line} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Line.prototype.equals = function (rhs) { @@ -92,7 +93,7 @@ dwv.math.Line.prototype.getWorldLength = function (spacingX, spacingY) { /** * Get the mid point of the line. * - * @returns {object} The mid point of the line. + * @returns {dwv.math.Point2D} The mid point of the line. */ dwv.math.Line.prototype.getMidpoint = function () { return new dwv.math.Point2D( @@ -137,8 +138,8 @@ dwv.math.Line.prototype.getInclination = function () { /** * Get the angle between two lines in degree. * - * @param {object} line0 The first line. - * @param {object} line1 The second line. + * @param {dwv.math.Line} line0 The first line. + * @param {dwv.math.Line} line1 The second line. * @returns {number} The angle. */ dwv.math.getAngle = function (line0, line1) { @@ -160,8 +161,8 @@ dwv.math.getAngle = function (line0, line1) { /** * Get a perpendicular line to an input one. * - * @param {object} line The line to be perpendicular to. - * @param {object} point The middle point of the perpendicular line. + * @param {dwv.math.Line} line The line to be perpendicular to. + * @param {dwv.math.Point2D} point The middle point of the perpendicular line. * @param {number} length The length of the perpendicular line. * @returns {object} A perpendicular line. */ diff --git a/src/math/matrix.js b/src/math/matrix.js index 3fc0d9287d..cccce4ed79 100644 --- a/src/math/matrix.js +++ b/src/math/matrix.js @@ -3,34 +3,35 @@ var dwv = dwv || {}; dwv.math = dwv.math || {}; // difference between 1 and the smallest floating point number greater than 1 +// -> ~2e-16 if (typeof Number.EPSILON === 'undefined') { Number.EPSILON = Math.pow(2, -52); } +// -> ~2e-12 +dwv.math.BIG_EPSILON = Number.EPSILON * 1e4; + +/** + * Check if two numbers are similar. + * + * @param {number} a The first number. + * @param {number} b The second number. + * @param {number} tol The comparison tolerance. + * @returns {boolean} True if similar. + */ +dwv.math.isSimilar = function (a, b, tol) { + if (typeof tol === 'undefined') { + tol = Number.EPSILON; + } + return Math.abs(a - b) < tol; +}; /** * Immutable 3x3 Matrix. * - * @param {number} m00 m[0][0] - * @param {number} m01 m[0][1] - * @param {number} m02 m[0][2] - * @param {number} m10 m[1][0] - * @param {number} m11 m[1][1] - * @param {number} m12 m[1][2] - * @param {number} m20 m[2][0] - * @param {number} m21 m[2][1] - * @param {number} m22 m[2][2] + * @param {Array} values row-major ordered 9 values. * @class */ -dwv.math.Matrix33 = function ( - m00, m01, m02, - m10, m11, m12, - m20, m21, m22) { - // row-major order - var mat = new Float32Array(9); - mat[0] = m00; mat[1] = m01; mat[2] = m02; - mat[3] = m10; mat[4] = m11; mat[5] = m12; - mat[6] = m20; mat[7] = m21; mat[8] = m22; - +dwv.math.Matrix33 = function (values) { /** * Get a value of the matrix. * @@ -39,32 +40,28 @@ dwv.math.Matrix33 = function ( * @returns {number} The value at the position. */ this.get = function (row, col) { - return mat[row * 3 + col]; + return values[row * 3 + col]; }; }; // Matrix33 /** * Check for Matrix33 equality. * - * @param {object} rhs The other matrix to compare to. + * @param {dwv.math.Matrix33} rhs The other matrix to compare to. * @param {number} p A numeric expression for the precision to use in check * (ex: 0.001). Defaults to Number.EPSILON if not provided. * @returns {boolean} True if both matrices are equal. */ dwv.math.Matrix33.prototype.equals = function (rhs, p) { - if (typeof p === 'undefined') { - p = Number.EPSILON; - } - - return Math.abs(this.get(0, 0) - rhs.get(0, 0)) < p && - Math.abs(this.get(0, 1) - rhs.get(0, 1)) < p && - Math.abs(this.get(0, 2) - rhs.get(0, 2)) < p && - Math.abs(this.get(1, 0) - rhs.get(1, 0)) < p && - Math.abs(this.get(1, 1) - rhs.get(1, 1)) < p && - Math.abs(this.get(1, 2) - rhs.get(1, 2)) < p && - Math.abs(this.get(2, 0) - rhs.get(2, 0)) < p && - Math.abs(this.get(2, 1) - rhs.get(2, 1)) < p && - Math.abs(this.get(2, 2) - rhs.get(2, 2)) < p; + return dwv.math.isSimilar(this.get(0, 0), rhs.get(0, 0), p) && + dwv.math.isSimilar(this.get(0, 1), rhs.get(0, 1), p) && + dwv.math.isSimilar(this.get(0, 2), rhs.get(0, 2), p) && + dwv.math.isSimilar(this.get(1, 0), rhs.get(1, 0), p) && + dwv.math.isSimilar(this.get(1, 1), rhs.get(1, 1), p) && + dwv.math.isSimilar(this.get(1, 2), rhs.get(1, 2), p) && + dwv.math.isSimilar(this.get(2, 0), rhs.get(2, 0), p) && + dwv.math.isSimilar(this.get(2, 1), rhs.get(2, 1), p) && + dwv.math.isSimilar(this.get(2, 2), rhs.get(2, 2), p); }; /** @@ -79,36 +76,308 @@ dwv.math.Matrix33.prototype.toString = function () { ']'; }; +/** + * Multiply this matrix by another. + * + * @param {dwv.math.Matrix33} rhs The matrix to multiply by. + * @returns {dwv.math.Matrix33} The product matrix. + */ +dwv.math.Matrix33.prototype.multiply = function (rhs) { + var values = []; + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 3; ++j) { + var tmp = 0; + for (var k = 0; k < 3; ++k) { + tmp += this.get(i, k) * rhs.get(k, j); + } + values.push(tmp); + } + } + return new dwv.math.Matrix33(values); +}; + +/** + * Get the absolute value of this matrix. + * + * @returns {dwv.math.Matrix33} The result matrix. + */ +dwv.math.Matrix33.prototype.getAbs = function () { + var values = []; + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 3; ++j) { + values.push(Math.abs(this.get(i, j))); + } + } + return new dwv.math.Matrix33(values); +}; + +/** + * Multiply this matrix by a 3D array. + * + * @param {Array} array3D The input 3D array. + * @returns {Array} The result 3D array. + */ +dwv.math.Matrix33.prototype.multiplyArray3D = function (array3D) { + if (array3D.length !== 3) { + throw new Error('Cannot multiply 3x3 matrix with non 3D array: ', + array3D.length); + } + var values = []; + for (var i = 0; i < 3; ++i) { + var tmp = 0; + for (var j = 0; j < 3; ++j) { + tmp += this.get(i, j) * array3D[j]; + } + values.push(tmp); + } + return values; +}; + /** * Multiply this matrix by a 3D vector. * - * @param {object} vector3D The input 3D vector - * @returns {object} The result 3D vector + * @param {dwv.math.Vector3D} vector3D The input 3D vector. + * @returns {dwv.math.Vector3D} The result 3D vector. + */ +dwv.math.Matrix33.prototype.multiplyVector3D = function (vector3D) { + var array3D = this.multiplyArray3D( + [vector3D.getX(), vector3D.getY(), vector3D.getZ()] + ); + return new dwv.math.Vector3D(array3D[0], array3D[1], array3D[2]); +}; + +/** + * Multiply this matrix by a 3D point. + * + * @param {dwv.math.Point3D} point3D The input 3D point. + * @returns {dwv.math.Point3D} The result 3D point. + */ +dwv.math.Matrix33.prototype.multiplyPoint3D = function (point3D) { + var array3D = this.multiplyArray3D( + [point3D.getX(), point3D.getY(), point3D.getZ()] + ); + return new dwv.math.Point3D(array3D[0], array3D[1], array3D[2]); +}; + +/** + * Multiply this matrix by a 3D index. + * + * @param {dwv.math.Index} index3D The input 3D index. + * @returns {dwv.math.Index} The result 3D index. + */ +dwv.math.Matrix33.prototype.multiplyIndex3D = function (index3D) { + var array3D = this.multiplyArray3D(index3D.getValues()); + return new dwv.math.Index(array3D); +}; + +/** + * Get the inverse of this matrix. + * + * @returns {dwv.math.Matrix33} The inverse matrix. + * @see https://en.wikipedia.org/wiki/Invertible_matrix#Inversion_of_3_%C3%97_3_matrices + */ +dwv.math.Matrix33.prototype.getInverse = function () { + var a = this.get(0, 0); + var b = this.get(0, 1); + var c = this.get(0, 2); + var d = this.get(1, 0); + var e = this.get(1, 1); + var f = this.get(1, 2); + var g = this.get(2, 0); + var h = this.get(2, 1); + var i = this.get(2, 2); + + var a2 = e * i - f * h; + var b2 = f * g - d * i; + var c2 = d * h - e * g; + + var det = a * a2 + b * b2 + c * c2; + if (det === 0) { + dwv.logger.warn('Cannot invert matrix with zero determinant.'); + return; + } + + var values = [ + a2 / det, + (c * h - b * i) / det, + (b * f - c * e) / det, + b2 / det, + (a * i - c * g) / det, + (c * d - a * f) / det, + c2 / det, + (b * g - a * h) / det, + (a * e - b * d) / det + ]; + + return new dwv.math.Matrix33(values); +}; + +/** + * Get the index of the maximum in absolute value of a row. + * + * @param {number} row The row to get the maximum from. + * @returns {object} The {value,index} of the maximum. + */ +dwv.math.Matrix33.prototype.getRowAbsMax = function (row) { + var values = [ + Math.abs(this.get(row, 0)), + Math.abs(this.get(row, 1)), + Math.abs(this.get(row, 2)) + ]; + var absMax = Math.max.apply(null, values); + var index = values.indexOf(absMax); + return { + value: this.get(row, index), + index: index + }; +}; + +/** + * Get the index of the maximum in absolute value of a column. + * + * @param {number} col The column to get the maximum from. + * @returns {object} The {value,index} of the maximum. + */ +dwv.math.Matrix33.prototype.getColAbsMax = function (col) { + var values = [ + Math.abs(this.get(0, col)), + Math.abs(this.get(1, col)), + Math.abs(this.get(2, col)) + ]; + var absMax = Math.max.apply(null, values); + var index = values.indexOf(absMax); + return { + value: this.get(index, col), + index: index + }; +}; + +/** + * Get this matrix with only zero and +/- ones instead of the maximum, + * + * @returns {dwv.math.Matrix33} The simplified matrix. */ -dwv.math.Matrix33.multiplyVector3D = function (vector3D) { - // cache matrix values - var m00 = this.get(0, 0); var m01 = this.get(0, 1); var m02 = this.get(0, 2); - var m10 = this.get(1, 0); var m11 = this.get(1, 1); var m12 = this.get(1, 2); - var m20 = this.get(2, 0); var m21 = this.get(2, 1); var m22 = this.get(2, 2); - // cache vector values - var vx = vector3D.getX(); - var vy = vector3D.getY(); - var vz = vector3D.getZ(); - // calculate - return new dwv.math.Vector3D( - (m00 * vx) + (m01 * vy) + (m02 * vz), - (m10 * vx) + (m11 * vy) + (m12 * vz), - (m20 * vx) + (m21 * vy) + (m22 * vz)); +dwv.math.Matrix33.prototype.asOneAndZeros = function () { + var res = []; + for (var j = 0; j < 3; ++j) { + var max = this.getRowAbsMax(j); + var sign = max.value > 0 ? 1 : -1; + for (var i = 0; i < 3; ++i) { + if (i === max.index) { + //res.push(1); + res.push(1 * sign); + } else { + res.push(0); + } + } + } + return new dwv.math.Matrix33(res); +}; + +/** + * Get the third column direction index of an orientation matrix. + * + * @returns {number} The index of the absolute maximum of the last column. + */ +dwv.math.Matrix33.prototype.getThirdColMajorDirection = function () { + return this.getColAbsMax(2).index; }; /** * Create a 3x3 identity matrix. * - * @returns {object} The identity matrix. + * @returns {dwv.math.Matrix33} The identity matrix. */ dwv.math.getIdentityMat33 = function () { - return new dwv.math.Matrix33( + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ 1, 0, 0, 0, 1, 0, - 0, 0, 1); + 0, 0, 1 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Check if a matrix is a 3x3 identity matrix. + * + * @param {dwv.math.Matrix33} mat33 The matrix to test. + * @returns {boolean} True if identity. + */ +dwv.math.isIdentityMat33 = function (mat33) { + return mat33.equals(dwv.math.getIdentityMat33()); +}; + +/** + * Create a 3x3 coronal (xzy) matrix. + * + * @returns {dwv.math.Matrix33} The coronal matrix. + */ +dwv.math.getCoronalMat33 = function () { + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ + 1, 0, 0, + 0, 0, 1, + 0, 1, 0 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Create a 3x3 sagittal (yzx) matrix. + * + * @returns {dwv.math.Matrix33} The sagittal matrix. + */ +dwv.math.getSagittalMat33 = function () { + /* eslint-disable array-element-newline */ + return new dwv.math.Matrix33([ + 0, 0, 1, + 1, 0, 0, + 0, 1, 0 + ]); + /* eslint-enable array-element-newline */ +}; + +/** + * Get an orientation matrix from a name. + * + * @param {string} name The orientation name. + * @returns {dwv.math.Matrix33} The orientation matrix. + */ +dwv.math.getMatrixFromName = function (name) { + var matrix = null; + if (name === 'axial') { + matrix = dwv.math.getIdentityMat33(); + } else if (name === 'coronal') { + matrix = dwv.math.getCoronalMat33(); + } else if (name === 'sagittal') { + matrix = dwv.math.getSagittalMat33(); + } + return matrix; +}; + +/** + * Get the oriented values of an input 3D array. + * + * @param {Array} array3D The 3D array. + * @param {dwv.math.Matrix33} orientation The orientation 3D matrix. + * @returns {Array} The values reordered according to the orientation. + */ +dwv.math.getOrientedArray3D = function (array3D, orientation) { + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + return orientation.getInverse().getAbs().multiplyArray3D(array3D); +}; + +/** + * Get the raw values of an oriented input 3D array. + * + * @param {Array} array3D The 3D array. + * @param {dwv.math.Matrix33} orientation The orientation 3D matrix. + * @returns {Array} The values reordered to compensate the orientation. + */ +dwv.math.getDeOrientedArray3D = function (array3D, orientation) { + // values = orientation * orientedValues + // -> inv(orientation) * values = orientedValues + return orientation.getAbs().multiplyArray3D(array3D); }; diff --git a/src/math/path.js b/src/math/path.js index 3bfe9a744a..8923ebf608 100644 --- a/src/math/path.js +++ b/src/math/path.js @@ -32,7 +32,7 @@ dwv.math.Path = function (inputPointArray, inputControlPointIndexArray) { * Get a point of the list. * * @param {number} index The index of the point to get (beware, no size check). - * @returns {object} The Point2D at the given index. + * @returns {dwv.math.Point2D} The Point2D at the given index. */ dwv.math.Path.prototype.getPoint = function (index) { return this.pointArray[index]; @@ -41,7 +41,7 @@ dwv.math.Path.prototype.getPoint = function (index) { /** * Is the given point a control point. * - * @param {object} point The Point2D to check. + * @param {dwv.math.Point2D} point The Point2D to check. * @returns {boolean} True if a control point. */ dwv.math.Path.prototype.isControlPoint = function (point) { @@ -65,7 +65,7 @@ dwv.math.Path.prototype.getLength = function () { /** * Add a point to the path. * - * @param {object} point The Point2D to add. + * @param {dwv.math.Point2D} point The Point2D to add. */ dwv.math.Path.prototype.addPoint = function (point) { this.pointArray.push(point); @@ -74,7 +74,7 @@ dwv.math.Path.prototype.addPoint = function (point) { /** * Add a control point to the path. * - * @param {object} point The Point2D to make a control point. + * @param {dwv.math.Point2D} point The Point2D to make a control point. */ dwv.math.Path.prototype.addControlPoint = function (point) { var index = this.pointArray.indexOf(point); @@ -82,7 +82,7 @@ dwv.math.Path.prototype.addControlPoint = function (point) { this.controlPointIndexArray.push(index); } else { throw new Error( - 'Error: addControlPoint called with no point in list point.'); + 'Cannot mark a non registered point as control point.'); } }; diff --git a/src/math/point.js b/src/math/point.js index 21d83e3cc9..136454d10c 100644 --- a/src/math/point.js +++ b/src/math/point.js @@ -31,7 +31,7 @@ dwv.math.Point2D = function (x, y) { /** * Check for Point2D equality. * - * @param {object} rhs The other point to compare to. + * @param {dwv.math.Point2D} rhs The other point to compare to. * @returns {boolean} True if both points are equal. */ dwv.math.Point2D.prototype.equals = function (rhs) { @@ -73,39 +73,6 @@ dwv.math.Point2D.prototype.getRound = function () { ); }; -/** - * Mutable 2D point. - * - * @class - * @param {number} x The X coordinate for the point. - * @param {number} y The Y coordinate for the point. - */ -dwv.math.FastPoint2D = function (x, y) { - this.x = x; - this.y = y; -}; // FastPoint2D class - -/** - * Check for FastPoint2D equality. - * - * @param {object} rhs The other point to compare to. - * @returns {boolean} True if both points are equal. - */ -dwv.math.FastPoint2D.prototype.equals = function (rhs) { - return rhs !== null && - this.x === rhs.x && - this.y === rhs.y; -}; - -/** - * Get a string representation of the FastPoint2D. - * - * @returns {string} The point as a string. - */ -dwv.math.FastPoint2D.prototype.toString = function () { - return '(' + this.x + ', ' + this.y + ')'; -}; - /** * Immutable 3D point. * @@ -144,7 +111,7 @@ dwv.math.Point3D = function (x, y, z) { /** * Check for Point3D equality. * - * @param {object} rhs The other point to compare to. + * @param {dwv.math.Point3D} rhs The other point to compare to. * @returns {boolean} True if both points are equal. */ dwv.math.Point3D.prototype.equals = function (rhs) { @@ -182,7 +149,7 @@ dwv.math.Point3D.prototype.getDistance = function (point3D) { * Get the difference to another Point3D. * * @param {dwv.math.Point3D} point3D The input point. - * @returns {object} The 3D vector from the input point to this one. + * @returns {dwv.math.Point3D} The 3D vector from the input point to this one. */ dwv.math.Point3D.prototype.minus = function (point3D) { return new dwv.math.Vector3D( @@ -192,60 +159,166 @@ dwv.math.Point3D.prototype.minus = function (point3D) { }; /** - * Immutable 3D index. + * Immutable point. + * Warning: the input array is NOT cloned, modifying it will + * modify the index values. * * @class - * @param {number} i The column index. - * @param {number} j The row index. - * @param {number} k The slice index. + * @param {Array} values The point values. */ -dwv.math.Index3D = function (i, j, k) { +dwv.math.Point = function (values) { + if (!values || typeof values === 'undefined') { + throw new Error('Cannot create point with no values.'); + } + if (values.length === 0) { + throw new Error('Cannot create point with empty values.'); + } + var valueCheck = function (val) { + return !isNaN(val); + }; + if (!values.every(valueCheck)) { + throw new Error('Cannot create point with non number values.'); + } + + /** + * Get the index value at the given array index. + * + * @param {number} i The index to get. + * @returns {number} The value. + */ + this.get = function (i) { + return values[i]; + }; + /** - * Get the column index. + * Get the length of the index. * - * @returns {number} The column index. + * @returns {number} The length. */ - this.getI = function () { - return i; + this.length = function () { + return values.length; }; + /** - * Get the row index. + * Get a string representation of the Index. * - * @returns {number} The row index. + * @returns {string} The Index as a string. */ - this.getJ = function () { - return j; + this.toString = function () { + return '(' + values.toString() + ')'; }; + /** - * Get the slice index. + * Get the values of this index. * - * @returns {number} The slice index. + * @returns {Array} The array of values. */ - this.getK = function () { - return k; + this.getValues = function () { + return values.slice(); }; -}; // Index3D class + +}; // Point class /** - * Check for Index3D equality. + * Check if the input point can be compared to this one. * - * @param {object} rhs The other index to compare to. - * @returns {boolean} True if both indices are equal. + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {boolean} True if both points are comparable. */ -dwv.math.Index3D.prototype.equals = function (rhs) { - return rhs !== null && - this.getI() === rhs.getI() && - this.getJ() === rhs.getJ() && - this.getK() === rhs.getK(); +dwv.math.Point.prototype.canCompare = function (rhs) { + // check input + if (!rhs) { + return false; + } + // check length + if (this.length() !== rhs.length()) { + return false; + } + // seems ok! + return true; +}; + +/** + * Check for Point equality. + * + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {boolean} True if both points are equal. + */ +dwv.math.Point.prototype.equals = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return false; + } + // check values + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + return false; + } + } + // seems ok! + return true; +}; + +/** + * Compare points and return different dimensions. + * + * @param {dwv.math.Point} rhs The point to compare to. + * @returns {Array} The list of different dimensions. + */ +dwv.math.Point.prototype.compare = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + // check values + var diffDims = []; + for (var i = 0, leni = this.length(); i < leni; ++i) { + if (this.get(i) !== rhs.get(i)) { + diffDims.push(i); + } + } + return diffDims; +}; + +/** + * Get the 3D part of this point. + * + * @returns {dwv.math.Point3D} The Point3D. + */ +dwv.math.Point.prototype.get3D = function () { + return new dwv.math.Point3D(this.get(0), this.get(1), this.get(2)); +}; + +/** + * Add another point to this one. + * + * @param {dwv.math.Point} rhs The point to add. + * @returns {dwv.math.Point} The point representing the sum of both points. + */ +dwv.math.Point.prototype.add = function (rhs) { + // check if can compare + if (!this.canCompare(rhs)) { + return null; + } + var values = []; + var values0 = this.getValues(); + var values1 = rhs.getValues(); + for (var i = 0; i < values0.length; ++i) { + values.push(values0[i] + values1[i]); + } + return new dwv.math.Point(values); }; /** - * Get a string representation of the Index3D. + * Merge this point with a Point3D to create a new point. * - * @returns {string} The Index3D as a string. + * @param {dwv.math.Point3D} rhs The Point3D to merge with. + * @returns {dwv.math.Point} The merge result. */ -dwv.math.Index3D.prototype.toString = function () { - return '(' + this.getI() + - ', ' + this.getJ() + - ', ' + this.getK() + ')'; +dwv.math.Point.prototype.mergeWith3D = function (rhs) { + var values = this.getValues(); + values[0] = rhs.getX(); + values[1] = rhs.getY(); + values[2] = rhs.getZ(); + return new dwv.math.Point(values); }; diff --git a/src/math/rectangle.js b/src/math/rectangle.js index eb94a9b40f..bf1e3d2369 100644 --- a/src/math/rectangle.js +++ b/src/math/rectangle.js @@ -23,8 +23,10 @@ dwv.math.mulABC = function (a, b, c) { * Rectangle shape. * * @class - * @param {object} begin A Point2D representing the beginning of the rectangle. - * @param {object} end A Point2D representing the end of the rectangle. + * @param {dwv.math.Point2D} begin A Point2D representing the beginning + * of the rectangle. + * @param {dwv.math.Point2D} end A Point2D representing the end + * of the rectangle. */ dwv.math.Rectangle = function (begin, end) { if (end.getX() < begin.getX()) { @@ -41,7 +43,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Get the begin point of the rectangle. * - * @returns {object} The begin point of the rectangle + * @returns {dwv.math.Point2D} The begin point of the rectangle */ this.getBegin = function () { return begin; @@ -50,7 +52,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Get the end point of the rectangle. * - * @returns {object} The end point of the rectangle + * @returns {dwv.math.Point2D} The end point of the rectangle */ this.getEnd = function () { return end; @@ -60,7 +62,7 @@ dwv.math.Rectangle = function (begin, end) { /** * Check for equality. * - * @param {object} rhs The object to compare to. + * @param {dwv.math.Rectangle} rhs The object to compare to. * @returns {boolean} True if both objects are equal. */ dwv.math.Rectangle.prototype.equals = function (rhs) { diff --git a/src/math/roi.js b/src/math/roi.js index 870bb8890a..3f4b4ce058 100644 --- a/src/math/roi.js +++ b/src/math/roi.js @@ -22,7 +22,7 @@ dwv.math.ROI = function () { * * @param {number} index The index of the point to get * (beware, no size check). - * @returns {object} The Point2D at the given index. + * @returns {dwv.math.Point2D} The Point2D at the given index. */ this.getPoint = function (index) { return points[index]; @@ -38,7 +38,7 @@ dwv.math.ROI = function () { /** * Add a point to the ROI. * - * @param {object} point The Point2D to add. + * @param {dwv.math.Point2D} point The Point2D to add. */ this.addPoint = function (point) { points.push(point); diff --git a/src/math/scissors.js b/src/math/scissors.js index d161779c5c..4e7fa5b2f7 100644 --- a/src/math/scissors.js +++ b/src/math/scissors.js @@ -202,8 +202,8 @@ dwv.math.gradUnitVector = function (gradX, gradY, px, py, out) { }; dwv.math.gradDirection = function (gradX, gradY, px, py, qx, qy) { - var __dgpuv = new dwv.math.FastPoint2D(-1, -1); - var __gdquv = new dwv.math.FastPoint2D(-1, -1); + var __dgpuv = {x: -1, y: -1}; + var __gdquv = {x: -1, y: -1}; // Compute the gradiant direction, in radians, between to points dwv.math.gradUnitVector(gradX, gradY, px, py, __dgpuv); dwv.math.gradUnitVector(gradX, gradY, qx, qy, __gdquv); @@ -236,7 +236,7 @@ dwv.math.computeSides = function (dist, gradX, gradY, greyscale) { sides.inside = []; sides.outside = []; - var guv = new dwv.math.FastPoint2D(-1, -1); // Current gradient unit vector + var guv = {x: -1, y: -1}; // Current gradient unit vector for (var y = 0; y < gradX.length; y++) { sides.inside[y] = []; @@ -520,7 +520,7 @@ dwv.math.Scissors.prototype.adj = function (p) { for (var y = sy; y <= ey; y++) { for (var x = sx; x <= ex; x++) { if (x !== p.x || y !== p.y) { - list[idx++] = new dwv.math.FastPoint2D(x, y); + list[idx++] = {x: x, y: y}; } } } diff --git a/src/math/vector.js b/src/math/vector.js index 68c0dc5b6f..42d5607374 100644 --- a/src/math/vector.js +++ b/src/math/vector.js @@ -45,9 +45,9 @@ dwv.math.Vector3D = function (x, y, z) { */ dwv.math.Vector3D.prototype.equals = function (rhs) { return rhs !== null && - this.getX() === rhs.getX() && - this.getY() === rhs.getY() && - this.getZ() === rhs.getZ(); + this.getX() === rhs.getX() && + this.getY() === rhs.getY() && + this.getZ() === rhs.getZ(); }; /** @@ -57,8 +57,8 @@ dwv.math.Vector3D.prototype.equals = function (rhs) { */ dwv.math.Vector3D.prototype.toString = function () { return '(' + this.getX() + - ', ' + this.getY() + - ', ' + this.getZ() + ')'; + ', ' + this.getY() + + ', ' + this.getZ() + ')'; }; /** @@ -67,9 +67,11 @@ dwv.math.Vector3D.prototype.toString = function () { * @returns {number} The norm. */ dwv.math.Vector3D.prototype.norm = function () { - return Math.sqrt((this.getX() * this.getX()) + - (this.getY() * this.getY()) + - (this.getZ() * this.getZ())); + return Math.sqrt( + (this.getX() * this.getX()) + + (this.getY() * this.getY()) + + (this.getZ() * this.getZ()) + ); }; /** @@ -77,8 +79,9 @@ dwv.math.Vector3D.prototype.norm = function () { * vector that is perpendicular to both a and b. * If both vectors are parallel, the cross product is a zero vector. * - * @param {object} vector3D The input vector. - * @returns {object} The result vector. + * @see https://en.wikipedia.org/wiki/Cross_product + * @param {dwv.math.Vector3D} vector3D The input vector. + * @returns {dwv.math.Vector3D} The result vector. */ dwv.math.Vector3D.prototype.crossProduct = function (vector3D) { return new dwv.math.Vector3D( @@ -90,11 +93,12 @@ dwv.math.Vector3D.prototype.crossProduct = function (vector3D) { /** * Get the dot product with another Vector3D. * - * @param {object} vector3D The input vector. + * @see https://en.wikipedia.org/wiki/Dot_product + * @param {dwv.math.Vector3D} vector3D The input vector. * @returns {number} The dot product. */ dwv.math.Vector3D.prototype.dotProduct = function (vector3D) { return (this.getX() * vector3D.getX()) + - (this.getY() * vector3D.getY()) + - (this.getZ() * vector3D.getZ()); + (this.getY() * vector3D.getY()) + + (this.getZ() * vector3D.getZ()); }; diff --git a/src/tools/circle.js b/src/tools/circle.js index ac2fce0f3d..a25468493a 100644 --- a/src/tools/circle.js +++ b/src/tools/circle.js @@ -278,6 +278,51 @@ dwv.tool.draw.CircleFactory.prototype.update = function ( group.add(dwv.tool.draw.getShadowCircle(circle, group)); } + // update label position + var textPos = {x: center.x, y: center.y}; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateCircleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Circle. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.CircleFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateCircleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Circle (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateCircleQuantification = function ( + group, viewController) { + // associated shape + var kcircle = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var centerPoint = new dwv.math.Point2D( + group.x() + kcircle.x(), + group.y() + kcircle.y() + ); + // circle + var circle = new dwv.math.Circle(centerPoint, kcircle.radius()); + // update text var ktext = klabel.getText(); var quantification = circle.quantify( @@ -286,15 +331,12 @@ dwv.tool.draw.CircleFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = {x: center.x, y: center.y}; - klabel.position(textPos); }; /** * Get the debug shadow. * - * @param {object} circle The circle to shadow. + * @param {dwv.math.Circle} circle The circle to shadow. * @param {object} group The associated group. * @returns {object} The shadow konva group. */ diff --git a/src/tools/draw.js b/src/tools/draw.js index 0fefb20ad2..0f343d3fbd 100644 --- a/src/tools/draw.js +++ b/src/tools/draw.js @@ -1,6 +1,7 @@ // namespaces var dwv = dwv || {}; dwv.tool = dwv.tool || {}; +dwv.tool.draw = dwv.tool.draw || {}; /** * The Konva namespace. * @@ -20,7 +21,7 @@ dwv.tool.draw.debug = false; * This tool is responsible for the draw layer group structure. The layout is: * * drawLayer - * |_ positionGroup: name="position-group", id="slice-#_frame-#"" + * |_ positionGroup: name="position-group", id="#2-0#_#3-1"" * |_ shapeGroup: name="{shape name}-group", id="#" * |_ shape: name="shape" * |_ label: name="label" @@ -35,7 +36,7 @@ dwv.tool.draw.debug = false; * cons: slice/frame display: 2 loops * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Draw = function (app) { /** @@ -155,14 +156,6 @@ dwv.tool.Draw = function (app) { */ var listeners = {}; - /** - * The associated Konva layer. - * - * @private - * @type {object} - */ - var konvaLayer = null; - /** * Handle mouse down event. * @@ -174,14 +167,15 @@ dwv.tool.Draw = function (app) { return; } - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var drawLayer = layerGroup.getActiveDrawLayer(); // determine if the click happened in an existing shape var stage = drawLayer.getKonvaStage(); var kshape = stage.getIntersection({ - x: event._xs, - y: event._ys + x: event._x, + y: event._y }); // update scale @@ -196,7 +190,7 @@ dwv.tool.Draw = function (app) { shapeEditor.disable(); shapeEditor.setShape(selectedShape); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); shapeEditor.setViewController(viewController); shapeEditor.enable(); } @@ -212,7 +206,9 @@ dwv.tool.Draw = function (app) { // clear array points = []; // store point - lastPoint = new dwv.math.Point2D(event._x, event._y); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); points.push(lastPoint); } }; @@ -228,9 +224,14 @@ dwv.tool.Draw = function (app) { return; } + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + // draw line to current pos - if (Math.abs(event._x - lastPoint.getX()) > 0 || - Math.abs(event._y - lastPoint.getY()) > 0) { + if (Math.abs(pos.x - lastPoint.getX()) > 0 || + Math.abs(pos.y - lastPoint.getY()) > 0) { // clear last added point from the list (but not the first one) // if it was marked as temporary if (points.length !== 1 && @@ -238,22 +239,22 @@ dwv.tool.Draw = function (app) { points.pop(); } // current point - lastPoint = new dwv.math.Point2D(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); // mark it as temporary lastPoint.tmp = true; // add it to the list points.push(lastPoint); // update points - onNewPoints(points); + onNewPoints(points, layerGroup); } }; /** * Handle mouse up event. * - * @param {object} _event The mouse up event. + * @param {object} event The mouse up event. */ - this.mouseup = function (_event) { + this.mouseup = function (event) { // exit if not started draw if (!started) { return; @@ -267,7 +268,9 @@ dwv.tool.Draw = function (app) { // do we have all the needed points if (points.length === currentFactory.getNPoints()) { // store points - onFinalPoints(points); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + onFinalPoints(points, layerGroup); // reset flag started = false; } else { @@ -281,9 +284,9 @@ dwv.tool.Draw = function (app) { /** * Handle double click event. * - * @param {object} _event The mouse up event. + * @param {object} event The mouse up event. */ - this.dblclick = function (_event) { + this.dblclick = function (event) { // exit if not started draw if (!started) { return; @@ -295,7 +298,9 @@ dwv.tool.Draw = function (app) { } // store points - onFinalPoints(points); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + onFinalPoints(points, layerGroup); // reset flag started = false; }; @@ -329,14 +334,19 @@ dwv.tool.Draw = function (app) { return; } - if (Math.abs(event._x - lastPoint.getX()) > 0 || - Math.abs(event._y - lastPoint.getY()) > 0) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var pos = viewLayer.displayToPlanePos(event._x, event._y); + + if (Math.abs(pos.x - lastPoint.getX()) > 0 || + Math.abs(pos.y - lastPoint.getY()) > 0) { // clear last added point from the list (but not the first one) if (points.length !== 1) { points.pop(); } // current point - lastPoint = new dwv.math.Point2D(event._x, event._y); + lastPoint = new dwv.math.Point2D(pos.x, pos.y); // add current one to the list points.push(lastPoint); // allow for anchor points @@ -347,7 +357,7 @@ dwv.tool.Draw = function (app) { }, currentFactory.getTimeout()); } // update points - onNewPoints(points); + onNewPoints(points, layerGroup); } }; @@ -371,11 +381,13 @@ dwv.tool.Draw = function (app) { event.context = 'dwv.tool.Draw'; app.onKeydown(event); } + var konvaLayer; // press delete key if (event.keyCode === 46 && shapeEditor.isActive()) { // get shape var shapeGroup = shapeEditor.getShape().getParent(); + konvaLayer = shapeGroup.getLayer(); var shapeDisplayName = dwv.tool.GetShapeDisplayName( shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0]); // delete command @@ -388,11 +400,11 @@ dwv.tool.Draw = function (app) { } // escape key: exit shape creation - if (event.keyCode === 27) { + if (event.keyCode === 27 && tmpShapeGroup !== null) { + konvaLayer = tmpShapeGroup.getLayer(); // reset temporary shape group - if (tmpShapeGroup) { - tmpShapeGroup.destroy(); - } + tmpShapeGroup.destroy(); + tmpShapeGroup = null; // reset flag and points started = false; points = []; @@ -405,16 +417,21 @@ dwv.tool.Draw = function (app) { * Update the current draw with new points. * * @param {Array} tmpPoints The array of new points. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function onNewPoints(tmpPoints) { + function onNewPoints(tmpPoints, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // remove temporary shape draw if (tmpShapeGroup) { tmpShapeGroup.destroy(); + tmpShapeGroup = null; } + // create shape group - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); tmpShapeGroup = currentFactory.create( tmpPoints, self.style, viewController); // do not listen during creation @@ -430,18 +447,22 @@ dwv.tool.Draw = function (app) { * Create the final shape from a point list. * * @param {Array} finalPoints The array of points. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function onFinalPoints(finalPoints) { + function onFinalPoints(finalPoints, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // reset temporary shape group if (tmpShapeGroup) { tmpShapeGroup.destroy(); + tmpShapeGroup = null; } - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); // create final shape var finalShapeGroup = currentFactory.create( @@ -466,7 +487,7 @@ dwv.tool.Draw = function (app) { app.addToUndoStack(command); // activate shape listeners - self.setShapeOn(finalShapeGroup); + self.setShapeOn(finalShapeGroup, layerGroup); } /** @@ -481,42 +502,42 @@ dwv.tool.Draw = function (app) { shapeEditor.setViewController(null); document.body.style.cursor = 'default'; // get the current draw layer - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); - konvaLayer = drawLayer.getKonvaLayer(); - activateCurrentPositionShapes(flag); + var layerGroup = app.getActiveLayerGroup(); + activateCurrentPositionShapes(flag, layerGroup); // listen to app change to update the draw layer if (flag) { - app.addEventListener('slicechange', updateDrawLayer); - app.addEventListener('framechange', updateDrawLayer); - - // init with the app window scale - this.style.setBaseScale(app.getBaseScale()); + // TODO: merge with drawController.activateDrawLayer? + app.addEventListener('positionchange', function () { + updateDrawLayer(layerGroup); + }); // same for colour this.setLineColour(this.style.getLineColour()); } else { - app.removeEventListener('slicechange', updateDrawLayer); - app.removeEventListener('framechange', updateDrawLayer); + app.removeEventListener('positionchange', function () { + updateDrawLayer(layerGroup); + }); } }; /** * Update the draw layer. + * + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function updateDrawLayer() { + function updateDrawLayer(layerGroup) { // activate the shape at current position - activateCurrentPositionShapes(true); + activateCurrentPositionShapes(true, layerGroup); } /** * Activate shapes at current position. * * @param {boolean} visible Set the draw layer visible or not. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - function activateCurrentPositionShapes(visible) { - var layerController = app.getLayerController(); + function activateCurrentPositionShapes(visible, layerGroup) { var drawController = - layerController.getActiveDrawLayer().getDrawController(); + layerGroup.getActiveDrawLayer().getDrawController(); // get shape groups at the current position var shapeGroups = @@ -526,7 +547,7 @@ dwv.tool.Draw = function (app) { if (visible) { // activate shape listeners shapeGroups.forEach(function (group) { - self.setShapeOn(group); + self.setShapeOn(group, layerGroup); }); } else { // de-activate shape listeners @@ -535,6 +556,8 @@ dwv.tool.Draw = function (app) { }); } // draw + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); konvaLayer.draw(); } @@ -557,14 +580,15 @@ dwv.tool.Draw = function (app) { /** * Get the real position from an event. + * TODO: use layer method? * - * @param {object} index The input index. - * @returns {object} The reasl position in the image. + * @param {object} index The input index as {x,y}. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. + * @returns {object} The real position in the image as {x,y}. * @private */ - function getRealPosition(index) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + function getRealPosition(index, layerGroup) { + var drawLayer = layerGroup.getActiveDrawLayer(); var stage = drawLayer.getKonvaStage(); return { x: stage.offset().x + index.x / stage.scale().x, @@ -576,8 +600,9 @@ dwv.tool.Draw = function (app) { * Set shape group on properties. * * @param {object} shapeGroup The shape group to set on. + * @param {dwv.gui.LayerGroup} layerGroup The origin layer group. */ - this.setShapeOn = function (shapeGroup) { + this.setShapeOn = function (shapeGroup, layerGroup) { // mouse over styling shapeGroup.on('mouseover', function () { document.body.style.cursor = 'pointer'; @@ -587,6 +612,9 @@ dwv.tool.Draw = function (app) { document.body.style.cursor = 'default'; }); + var drawLayer = layerGroup.getActiveDrawLayer(); + var konvaLayer = drawLayer.getKonvaLayer(); + // make it draggable shapeGroup.draggable(true); // cache drag start position @@ -603,8 +631,7 @@ dwv.tool.Draw = function (app) { // store colour colour = shapeGroup.getChildren(dwv.draw.isNodeNameShape)[0].stroke(); // display trash - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var stage = drawLayer.getKonvaStage(); var scale = stage.scale(); var invscale = {x: 1 / scale.x, y: 1 / scale.y}; @@ -619,18 +646,22 @@ dwv.tool.Draw = function (app) { }); // drag move event handling shapeGroup.on('dragmove.draw', function (event) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); // validate the group position - dwv.tool.validateGroupPosition(drawLayer.getSize(), this); + dwv.tool.validateGroupPosition(drawLayer.getBaseSize(), this); + // update quantification if possible + if (typeof currentFactory.updateQuantification !== 'undefined') { + var vc = layerGroup.getActiveViewLayer().getViewController(); + currentFactory.updateQuantification(this, vc); + } // highlight trash when on it var offset = dwv.gui.getEventOffset(event.evt)[0]; - var eventPos = getRealPosition(offset); + var eventPos = getRealPosition(offset, layerGroup); var trashHalfWidth = trash.width() * trash.scaleX() / 2; var trashHalfHeight = trash.height() * trash.scaleY() / 2; if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth && - Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { - trash.getChildren().each(function (tshape) { + Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { + trash.getChildren().forEach(function (tshape) { tshape.stroke('orange'); }); // change the group shapes colour @@ -639,7 +670,7 @@ dwv.tool.Draw = function (app) { ashape.stroke('red'); }); } else { - trash.getChildren().each(function (tshape) { + trash.getChildren().forEach(function (tshape) { tshape.stroke('red'); }); // reset the group shapes colour @@ -660,11 +691,11 @@ dwv.tool.Draw = function (app) { trash.remove(); // delete case var offset = dwv.gui.getEventOffset(event.evt)[0]; - var eventPos = getRealPosition(offset); + var eventPos = getRealPosition(offset, layerGroup); var trashHalfWidth = trash.width() * trash.scaleX() / 2; var trashHalfHeight = trash.height() * trash.scaleY() / 2; if (Math.abs(eventPos.x - trash.x()) < trashHalfWidth && - Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { + Math.abs(eventPos.y - trash.y()) < trashHalfHeight) { // compensate for the drag translation this.x(dragStartPos.x); this.y(dragStartPos.y); @@ -739,11 +770,11 @@ dwv.tool.Draw = function (app) { }; // call client dialog if defined - if (typeof dwv.gui.openRoiDialog !== 'undefined') { - dwv.gui.openRoiDialog(ktext.meta, onSaveCallback); + if (typeof dwv.openRoiDialog !== 'undefined') { + dwv.openRoiDialog(ktext.meta, onSaveCallback); } else { // simple prompt for the text expression - var textExpr = prompt('Label', ktext.meta.textExpr); + var textExpr = dwv.prompt('Label', ktext.meta.textExpr); if (textExpr !== null) { ktext.meta.textExpr = textExpr; onSaveCallback(ktext.meta); @@ -876,7 +907,7 @@ dwv.tool.Draw.prototype.hasShape = function (name) { * Get the minimum position in a groups' anchors. * * @param {object} group The group that contains anchors. - * @returns {object} The minimum position. + * @returns {object} The minimum position as {x,y}. */ dwv.tool.getAnchorMin = function (group) { var anchors = group.find('.anchor'); @@ -885,10 +916,11 @@ dwv.tool.getAnchorMin = function (group) { } var minX = anchors[0].x(); var minY = anchors[0].y(); - anchors.each(function (anchor) { - minX = Math.min(minX, anchor.x()); - minY = Math.min(minY, anchor.y()); - }); + for (var i = 0; i < anchors.length; ++i) { + minX = Math.min(minX, anchors[i].x()); + minY = Math.min(minY, anchors[i].y()); + } + return {x: minX, y: minY}; }; @@ -896,8 +928,8 @@ dwv.tool.getAnchorMin = function (group) { * Bound a node position. * * @param {object} node The node to bound the position. - * @param {object} min The minimum position. - * @param {object} max The maximum position. + * @param {object} min The minimum position as {x,y}. + * @param {object} max The maximum position as {x,y}. * @returns {boolean} True if the position was corrected. */ dwv.tool.boundNodePosition = function (node, min, max) { @@ -930,6 +962,11 @@ dwv.tool.validateGroupPosition = function (stageSize, group) { // if anchors get mixed, width/height can be negative var shape = group.getChildren(dwv.draw.isNodeNameShape)[0]; var anchorMin = dwv.tool.getAnchorMin(group); + // handle no anchor: when dragging the label, the editor does + // not activate + if (typeof anchorMin === 'undefined') { + return null; + } var min = { x: -anchorMin.x, diff --git a/src/tools/drawCommands.js b/src/tools/drawCommands.js index 9569841526..dc08c058c6 100644 --- a/src/tools/drawCommands.js +++ b/src/tools/drawCommands.js @@ -40,7 +40,7 @@ dwv.tool.GetShapeDisplayName = function (shape) { * @param {object} group The group draw. * @param {string} name The shape display name. * @param {object} layer The layer where to draw the group. - * @param {object} silent Whether to send a creation event or not. + * @param {boolean} silent Whether to send a creation event or not. * @class */ dwv.tool.DrawGroupCommand = function (group, name, layer, silent) { diff --git a/src/tools/editor.js b/src/tools/editor.js index 1a7ccc4e36..56cfdf66b0 100644 --- a/src/tools/editor.js +++ b/src/tools/editor.js @@ -1,6 +1,7 @@ // namespaces var dwv = dwv || {}; dwv.tool = dwv.tool || {}; +dwv.tool.draw = dwv.tool.draw || {}; /** * The Konva namespace. * @@ -207,7 +208,7 @@ dwv.tool.ShapeEditor = function (app) { function applyFuncToAnchors(func) { if (shape && shape.getParent()) { var anchors = shape.getParent().find('.anchor'); - anchors.each(func); + anchors.forEach(func); } } @@ -326,10 +327,11 @@ dwv.tool.ShapeEditor = function (app) { }); // drag move listener anchor.on('dragmove.edit', function (evt) { - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(evt.evt); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var drawLayer = layerGroup.getActiveDrawLayer(); // validate the anchor position - dwv.tool.validateAnchorPosition(drawLayer.getSize(), this); + dwv.tool.validateAnchorPosition(drawLayer.getBaseSize(), this); // update shape currentFactory.update(this, app.getStyle(), viewController); // redraw diff --git a/src/tools/ellipse.js b/src/tools/ellipse.js index ac2173b92b..78fb3c79c3 100644 --- a/src/tools/ellipse.js +++ b/src/tools/ellipse.js @@ -266,6 +266,52 @@ dwv.tool.draw.EllipseFactory.prototype.update = function ( group.add(dwv.tool.draw.getShadowEllipse(ellipse, group)); } + // update label position + var textPos = {x: center.x, y: center.y}; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateEllipseQuantification(group, viewController); +}; + +/** + * Update the quantification of an Ellipse. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.EllipseFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateEllipseQuantification(group, viewController); +}; + +/** + * Update the quantification of an Ellipse (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateEllipseQuantification = function ( + group, viewController) { + // associated shape + var kellipse = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var centerPoint = new dwv.math.Point2D( + group.x() + kellipse.x(), + group.y() + kellipse.y() + ); + // circle + var ellipse = new dwv.math.Ellipse( + centerPoint, kellipse.radius().x, kellipse.radius().y); + // update text var ktext = klabel.getText(); var quantification = ellipse.quantify( @@ -274,15 +320,12 @@ dwv.tool.draw.EllipseFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = {x: center.x, y: center.y}; - klabel.position(textPos); }; /** * Get the debug shadow. * - * @param {object} ellipse The ellipse to shadow. + * @param {dwv.math.Ellipse} ellipse The ellipse to shadow. * @param {object} group The associated group. * @returns {object} The shadow konva group. */ diff --git a/src/tools/filter.js b/src/tools/filter.js index 6266ea35d1..d78e410674 100644 --- a/src/tools/filter.js +++ b/src/tools/filter.js @@ -8,7 +8,7 @@ dwv.tool.filter = dwv.tool.filter || {}; * Filter tool. * * @class - * @param {object} app The associated app. + * @param {dwv.App} app The associated app. */ dwv.tool.Filter = function (app) { /** @@ -178,7 +178,7 @@ dwv.tool.Filter.prototype.hasFilter = function (name) { * Threshold filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Threshold = function (app) { /** @@ -232,7 +232,7 @@ dwv.tool.filter.Threshold = function (app) { filter.setMax(args.max); // reset the image if asked if (resetImage) { - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); resetImage = false; } var command = new dwv.tool.RunFilterCommand(filter, app); @@ -280,7 +280,7 @@ dwv.tool.filter.Threshold = function (app) { * Sharpen filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Sharpen = function (app) { /** @@ -314,7 +314,7 @@ dwv.tool.filter.Sharpen = function (app) { */ this.run = function (_args) { var filter = new dwv.image.filter.Sharpen(); - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); var command = new dwv.tool.RunFilterCommand(filter, app); command.onExecute = fireEvent; command.onUndo = fireEvent; @@ -359,7 +359,7 @@ dwv.tool.filter.Sharpen = function (app) { * Sobel filter tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.filter.Sobel = function (app) { /** @@ -393,7 +393,7 @@ dwv.tool.filter.Sobel = function (app) { */ dwv.tool.filter.Sobel.prototype.run = function (_args) { var filter = new dwv.image.filter.Sobel(); - filter.setOriginalImage(app.getImage()); + filter.setOriginalImage(app.getLastImage()); var command = new dwv.tool.RunFilterCommand(filter, app); command.onExecute = fireEvent; command.onUndo = fireEvent; @@ -439,7 +439,7 @@ dwv.tool.filter.Sobel = function (app) { * * @class * @param {object} filter The filter to run. - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.RunFilterCommand = function (filter, app) { @@ -459,9 +459,9 @@ dwv.tool.RunFilterCommand = function (filter, app) { */ this.execute = function () { // run filter and set app image - app.setImage(filter.update()); + app.setLastImage(filter.update()); // update display - app.render(); + app.render(0); //todo: fix /** * Filter run event. * @@ -485,9 +485,9 @@ dwv.tool.RunFilterCommand = function (filter, app) { */ this.undo = function () { // reset the image - app.setImage(filter.getOriginalImage()); + app.setLastImage(filter.getOriginalImage()); // update display - app.render(); + app.render(0); //todo: fix /** * Filter undo event. * diff --git a/src/tools/floodfill.js b/src/tools/floodfill.js index b449432310..02282006a8 100644 --- a/src/tools/floodfill.js +++ b/src/tools/floodfill.js @@ -13,7 +13,7 @@ var MagicWand = MagicWand || {}; * Floodfill painting tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Floodfill = function (app) { /** @@ -168,7 +168,14 @@ dwv.tool.Floodfill = function (app) { * @private */ var getCoord = function (event) { - return {x: event._x, y: event._y}; + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + return { + x: index.get(0), + y: index.get(1) + }; }; /** @@ -190,7 +197,6 @@ dwv.tool.Floodfill = function (app) { bytes: 4 }; - // var p = new dwv.math.FastPoint2D(points.x, points.y); mask = MagicWand.floodFill(image, points.x, points.y, threshold); mask = MagicWand.gaussBlurOnlyBorder(mask, blurRadius); @@ -219,9 +225,10 @@ dwv.tool.Floodfill = function (app) { * @private * @param {object} point The start point. * @param {number} threshold The border threshold. + * @param {object} layerGroup The origin layer group. * @returns {boolean} False if no border. */ - var paintBorder = function (point, threshold) { + var paintBorder = function (point, threshold, layerGroup) { // Calculate the border border = calcBorder(point, threshold); // Paint the border @@ -230,8 +237,7 @@ dwv.tool.Floodfill = function (app) { shapeGroup = factory.create(border, self.style); shapeGroup.id(dwv.math.guid()); - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var drawController = drawLayer.getDrawController(); // get the position group @@ -260,8 +266,9 @@ dwv.tool.Floodfill = function (app) { * * @param {number} ini The first slice to extend to. * @param {number} end The last slice to extend to. + * @param {object} layerGroup The origin layer group. */ - this.extend = function (ini, end) { + this.extend = function (ini, end, layerGroup) { //avoid errors if (!initialpoint) { throw '\'initialpoint\' not found. User must click before use extend!'; @@ -271,31 +278,31 @@ dwv.tool.Floodfill = function (app) { shapeGroup.destroy(); } - var layerController = app.getLayerController(); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); - var pos = viewController.getCurrentPosition(); + var pos = viewController.getCurrentIndex(); + var imageSize = viewController.getImageSize(); var threshold = currentthreshold || initialthreshold; // Iterate over the next images and paint border on each slice. - for (var i = pos.k, + for (var i = pos.get(2), len = end - ? end : app.getImage().getGeometry().getSize().getNumberOfSlices(); + ? end : imageSize.get(2); i < len; i++) { - if (!paintBorder(initialpoint, threshold)) { + if (!paintBorder(initialpoint, threshold, layerGroup)) { break; } - viewController.incrementSliceNb(); + viewController.incrementIndex(2); } viewController.setCurrentPosition(pos); // Iterate over the prev images and paint border on each slice. - for (var j = pos.k, jl = ini ? ini : 0; j > jl; j--) { - if (!paintBorder(initialpoint, threshold)) { + for (var j = pos.get(2), jl = ini ? ini : 0; j > jl; j--) { + if (!paintBorder(initialpoint, threshold, layerGroup)) { break; } - viewController.decrementSliceNb(); + viewController.decrementIndex(2); } viewController.setCurrentPosition(pos); }; @@ -349,9 +356,10 @@ dwv.tool.Floodfill = function (app) { * @param {object} event The mouse down event. */ this.mousedown = function (event) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); - var drawLayer = layerController.getActiveDrawLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); imageInfo = viewLayer.getImageData(); if (!imageInfo) { @@ -365,7 +373,7 @@ dwv.tool.Floodfill = function (app) { self.started = true; initialpoint = getCoord(event); - paintBorder(initialpoint, initialthreshold); + paintBorder(initialpoint, initialthreshold, layerGroup); self.onThresholdChange(initialthreshold); }; @@ -395,7 +403,9 @@ dwv.tool.Floodfill = function (app) { this.mouseup = function (_event) { self.started = false; if (extender) { - self.extend(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + self.extend(layerGroup); } }; diff --git a/src/tools/livewire.js b/src/tools/livewire.js index 3cfef6c18b..3c4a05a362 100644 --- a/src/tools/livewire.js +++ b/src/tools/livewire.js @@ -6,7 +6,7 @@ dwv.tool = dwv.tool || {}; * Livewire painting tool. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Livewire = function (app) { /** @@ -84,10 +84,11 @@ dwv.tool.Livewire = function (app) { /** * Clear the parent points list. * + * @param {object} imageSize The image size. * @private */ - function clearParentPoints() { - var nrows = app.getImage().getGeometry().getSize().getNumberOfRows(); + function clearParentPoints(imageSize) { + var nrows = imageSize.get(1); for (var i = 0; i < nrows; ++i) { parentPoints[i] = []; } @@ -117,31 +118,36 @@ dwv.tool.Livewire = function (app) { * @param {object} event The mouse down event. */ this.mousedown = function (event) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var imageSize = viewLayer.getViewController().getImageSize(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + // first time if (!self.started) { self.started = true; - self.x0 = event._x; - self.y0 = event._y; + self.x0 = index.get(0); + self.y0 = index.get(1); // clear vars clearPaths(); - clearParentPoints(); + clearParentPoints(imageSize); shapeGroup = null; // update zoom scale - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); self.style.setZoomScale( drawLayer.getKonvaLayer().getAbsoluteScale()); // do the training from the first point - var p = new dwv.math.FastPoint2D(event._x, event._y); + var p = {x: index.get(0), y: index.get(1)}; scissors.doTraining(p); // add the initial point to the path - var p0 = new dwv.math.Point2D(event._x, event._y); + var p0 = new dwv.math.Point2D(index.get(0), index.get(1)); path.addPoint(p0); path.addControlPoint(p0); } else { // final point: at 'tolerance' of the initial point - if ((Math.abs(event._x - self.x0) < tolerance) && - (Math.abs(event._y - self.y0) < tolerance)) { + if ((Math.abs(index.get(0) - self.x0) < tolerance) && + (Math.abs(index.get(1) - self.y0) < tolerance)) { // draw self.mousemove(event); // listen @@ -156,8 +162,8 @@ dwv.tool.Livewire = function (app) { } else { // anchor point path = currentPath; - clearParentPoints(); - var pn = new dwv.math.FastPoint2D(event._x, event._y); + clearParentPoints(imageSize); + var pn = {x: index.get(0), y: index.get(1)}; scissors.doTraining(pn); path.addControlPoint(currentPath.getPoint(0)); } @@ -173,8 +179,13 @@ dwv.tool.Livewire = function (app) { if (!self.started) { return; } + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + // set the point to find the path to - var p = new dwv.math.FastPoint2D(event._x, event._y); + var p = {x: index.get(0), y: index.get(1)}; scissors.setPoint(p); // do the work var results = 0; @@ -222,8 +233,7 @@ dwv.tool.Livewire = function (app) { shapeGroup = factory.create(currentPath.pointArray, self.style); shapeGroup.id(dwv.math.guid()); - var layerController = app.getLayerController(); - var drawLayer = layerController.getActiveDrawLayer(); + var drawLayer = layerGroup.getActiveDrawLayer(); var drawController = drawLayer.getDrawController(); // get the position group @@ -318,14 +328,14 @@ dwv.tool.Livewire = function (app) { this.activate = function (bool) { // start scissors if displayed if (bool) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); + var layerGroup = app.getActiveLayerGroup(); + var viewLayer = layerGroup.getActiveViewLayer(); //scissors = new dwv.math.Scissors(); - var size = app.getImage().getGeometry().getSize(); + var imageSize = viewLayer.getViewController().getImageSize(); scissors.setDimensions( - size.getNumberOfColumns(), - size.getNumberOfRows()); + imageSize.get(0), + imageSize.get(1)); scissors.setData(viewLayer.getImageData().data); // init with the app window scale diff --git a/src/tools/opacity.js b/src/tools/opacity.js index 2b3cb5d041..8c36750765 100644 --- a/src/tools/opacity.js +++ b/src/tools/opacity.js @@ -6,7 +6,7 @@ dwv.tool = dwv.tool || {}; * Opacity class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Opacity = function (app) { /** @@ -51,8 +51,9 @@ dwv.tool.Opacity = function (app) { var xMove = (Math.abs(diffX) > 15); // do not trigger for small moves if (xMove) { - var layerController = app.getLayerController(); - var viewLayer = layerController.getActiveViewLayer(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); var op = viewLayer.getOpacity(); viewLayer.setOpacity(op + (diffX / 200)); viewLayer.draw(); diff --git a/src/tools/rectangle.js b/src/tools/rectangle.js index 0b97c35d40..88794a9b31 100644 --- a/src/tools/rectangle.js +++ b/src/tools/rectangle.js @@ -269,6 +269,58 @@ dwv.tool.draw.RectangleFactory.prototype.update = function ( kshadow.size({width: rWidth, height: rHeight}); } + // update label position + var textPos = { + x: rect.getBegin().getX() - group.x(), + y: rect.getEnd().getY() - group.y() + }; + klabel.position(textPos); + + // update quantification + dwv.tool.draw.updateRectangleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Rectangle. + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.RectangleFactory.prototype.updateQuantification = function ( + group, viewController) { + dwv.tool.draw.updateRectangleQuantification(group, viewController); +}; + +/** + * Update the quantification of a Rectangle (as a static + * function to be used in update). + * + * @param {object} group The group with the shape. + * @param {object} viewController The associated view controller. + */ +dwv.tool.draw.updateRectangleQuantification = function ( + group, viewController) { + // associated shape + var krect = group.getChildren(function (node) { + return node.name() === 'shape'; + })[0]; + // associated label + var klabel = group.getChildren(function (node) { + return node.name() === 'label'; + })[0]; + + // positions: add possible group offset + var p2d0 = new dwv.math.Point2D( + group.x() + krect.x(), + group.y() + krect.y() + ); + var p2d1 = new dwv.math.Point2D( + p2d0.getX() + krect.width(), + p2d0.getY() + krect.height() + ); + // rectangle + var rect = new dwv.math.Rectangle(p2d0, p2d1); + // update text var ktext = klabel.getText(); var quantification = rect.quantify( @@ -277,12 +329,6 @@ dwv.tool.draw.RectangleFactory.prototype.update = function ( ktext.setText(dwv.utils.replaceFlags(ktext.meta.textExpr, quantification)); // update meta ktext.meta.quantification = quantification; - // update position - var textPos = { - x: rect.getBegin().getX() - group.x(), - y: rect.getEnd().getY() - group.y() - }; - klabel.position(textPos); }; /** diff --git a/src/tools/scroll.js b/src/tools/scroll.js index 06721e565e..f901920704 100644 --- a/src/tools/scroll.js +++ b/src/tools/scroll.js @@ -6,7 +6,7 @@ dwv.tool = dwv.tool || {}; * Scroll class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.Scroll = function (app) { /** @@ -32,9 +32,10 @@ dwv.tool.Scroll = function (app) { */ this.mousedown = function (event) { // stop viewer if playing - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); if (viewController.isPlaying()) { viewController.stop(); } @@ -43,6 +44,10 @@ dwv.tool.Scroll = function (app) { // first position self.x0 = event._x; self.y0 = event._y; + + // update controller position + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + viewController.setCurrentPosition2D(planePos.x, planePos.y); }; /** @@ -55,20 +60,21 @@ dwv.tool.Scroll = function (app) { return; } - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); // difference to last Y position var diffY = event._y - self.y0; var yMove = (Math.abs(diffY) > 15); // do not trigger for small moves - if (yMove) { + if (yMove && viewController.canScroll()) { // update view controller if (diffY > 0) { - viewController.decrementSliceNb(); + viewController.decrementScrollIndex(); } else { - viewController.incrementSliceNb(); + viewController.incrementScrollIndex(); } } @@ -76,12 +82,13 @@ dwv.tool.Scroll = function (app) { var diffX = event._x - self.x0; var xMove = (Math.abs(diffX) > 15); // do not trigger for small moves - if (xMove) { + var imageSize = viewController.getImageSize(); + if (xMove && imageSize.moreThanOne(3)) { // update view controller if (diffX > 0) { - viewController.incrementFrameNb(); + viewController.decrementIndex(3); } else { - viewController.decrementFrameNb(); + viewController.incrementIndex(3); } } @@ -163,41 +170,21 @@ dwv.tool.Scroll = function (app) { * @param {object} event The mouse wheel event. */ this.wheel = function (event) { + var up = false; if (event.deltaY < 0) { - mouseScroll(true); - } else { - mouseScroll(false); + up = true; } - }; - /** - * Mouse scroll action. - * - * @param {boolean} up True to increment, false to decrement. - * @private - */ - function mouseScroll(up) { - var layerController = app.getLayerController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); - - var hasSlices = - (app.getImage().getGeometry().getSize().getNumberOfSlices() !== 1); - var hasFrames = (app.getImage().getNumberOfFrames() !== 1); + layerGroup.getActiveViewLayer().getViewController(); if (up) { - if (hasSlices) { - viewController.incrementSliceNb(); - } else if (hasFrames) { - viewController.incrementFrameNb(); - } + viewController.incrementScrollIndex(); } else { - if (hasSlices) { - viewController.decrementSliceNb(); - } else if (hasFrames) { - viewController.decrementFrameNb(); - } + viewController.decrementScrollIndex(); } - } + }; /** * Handle key down event. @@ -211,12 +198,13 @@ dwv.tool.Scroll = function (app) { /** * Handle double click. * - * @param {object} _event The key down event. + * @param {object} event The key down event. */ - this.dblclick = function (_event) { - var layerController = app.getLayerController(); + this.dblclick = function (event) { + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); viewController.play(); }; diff --git a/src/tools/windowLevel.js b/src/tools/windowLevel.js index 428f9c06b0..3e3bd8775b 100644 --- a/src/tools/windowLevel.js +++ b/src/tools/windowLevel.js @@ -6,7 +6,7 @@ dwv.tool = dwv.tool || {}; * WindowLevel tool: handle window/level related events. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.WindowLevel = function (app) { /** @@ -34,11 +34,6 @@ dwv.tool.WindowLevel = function (app) { // store initial position self.x0 = event._x; self.y0 = event._y; - // update view controller - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); - viewController.setCurrentPosition2D(event._x, event._y); }; /** @@ -52,9 +47,10 @@ dwv.tool.WindowLevel = function (app) { return; } - var layerController = app.getLayerController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); var viewController = - layerController.getActiveViewLayer().getViewController(); + layerGroup.getActiveViewLayer().getViewController(); // difference to last position var diffX = event._x - self.x0; @@ -70,7 +66,7 @@ dwv.tool.WindowLevel = function (app) { // add the manual preset to the view viewController.addWindowLevelPresets({ manual: { - wl: new dwv.image.WindowLevel(windowCenter, windowWidth), + wl: [new dwv.image.WindowLevel(windowCenter, windowWidth)], name: 'manual' } }); @@ -136,16 +132,20 @@ dwv.tool.WindowLevel = function (app) { * @param {object} event The double click event. */ this.dblclick = function (event) { - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var index = viewLayer.displayToPlaneIndex(event._x, event._y); + var viewController = viewLayer.getViewController(); + var image = app.getImage(viewLayer.getDataIndex()); // update view controller viewController.setWindowLevel( - parseInt(app.getImage().getRescaledValue( - event._x, - event._y, - viewController.getCurrentPosition().k + parseInt(image.getRescaledValueAtIndex( + viewController.getCurrentIndex().getWithNew2D( + index.get(0), + index.get(1) + ) ), 10), parseInt(viewController.getWindowLevel().width, 10)); }; diff --git a/src/tools/zoomPan.js b/src/tools/zoomPan.js index 4c32627de2..f54dc03d5d 100644 --- a/src/tools/zoomPan.js +++ b/src/tools/zoomPan.js @@ -6,7 +6,7 @@ dwv.tool = dwv.tool || {}; * ZoomAndPan class. * * @class - * @param {object} app The associated application. + * @param {dwv.App} app The associated application. */ dwv.tool.ZoomAndPan = function (app) { /** @@ -31,8 +31,8 @@ dwv.tool.ZoomAndPan = function (app) { this.mousedown = function (event) { self.started = true; // first position - self.x0 = event._xs; - self.y0 = event._ys; + self.x0 = event._x; + self.y0 = event._y; }; /** @@ -62,13 +62,24 @@ dwv.tool.ZoomAndPan = function (app) { return; } // calculate translation - var tx = event._xs - self.x0; - var ty = event._ys - self.y0; + var tx = event._x - self.x0; + var ty = event._y - self.y0; // apply translation - app.translate(tx, ty); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + var planeOffset = viewLayer.displayToPlaneScale(tx, ty); + var offset3D = viewController.getOffset3DFromPlaneOffset(planeOffset); + layerGroup.addTranslation({ + x: offset3D.getX(), + y: offset3D.getY(), + z: offset3D.getZ() + }); + layerGroup.draw(); // reset origin point - self.x0 = event._xs; - self.y0 = event._ys; + self.x0 = event._x; + self.y0 = event._y; }; /** @@ -85,6 +96,11 @@ dwv.tool.ZoomAndPan = function (app) { var newLine = new dwv.math.Line(point0, point1); var lineRatio = newLine.getLength() / self.line0.getLength(); + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + if (lineRatio === 1) { // scroll mode // difference to last position @@ -93,20 +109,23 @@ dwv.tool.ZoomAndPan = function (app) { if (Math.abs(diffY) < 15) { return; } - var layerController = app.getLayerController(); - var viewController = - layerController.getActiveViewLayer().getViewController(); + var imageSize = viewController.getImageSize(); // update view controller - if (diffY > 0) { - viewController.incrementSliceNb(); - } else { - viewController.decrementSliceNb(); + if (imageSize.canScroll(2)) { + if (diffY > 0) { + viewController.incrementIndex(2); + } else { + viewController.decrementIndex(2); + } } } else { // zoom mode var zoom = (lineRatio - 1) / 2; if (Math.abs(zoom) % 0.1 <= 0.05) { - app.zoom(zoom, event._xs, event._ys); + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + var center = viewController.getPositionFromPlanePoint(planePos); + layerGroup.addScale(zoom, center); + layerGroup.draw(); } } }; @@ -176,7 +195,15 @@ dwv.tool.ZoomAndPan = function (app) { */ this.wheel = function (event) { var step = -event.deltaY / 500; - app.zoom(step, event._xs, event._ys); + + var layerDetails = dwv.gui.getLayerDetailsFromEvent(event); + var layerGroup = app.getLayerGroupById(layerDetails.groupId); + var viewLayer = layerGroup.getActiveViewLayer(); + var viewController = viewLayer.getViewController(); + var planePos = viewLayer.displayToPlanePos(event._x, event._y); + var center = viewController.getPlanePositionFromPlanePoint(planePos); + layerGroup.addScale(step, center); + layerGroup.draw(); }; /** diff --git a/src/utils/colour.js b/src/utils/colour.js index 592a138ada..4144dc358b 100644 --- a/src/utils/colour.js +++ b/src/utils/colour.js @@ -262,3 +262,28 @@ dwv.utils.cielabToSrgb = function (triplet) { dwv.utils.srgbToCielab = function (triplet) { return dwv.utils.ciexyzToCielab(dwv.utils.srgbToCiexyz(triplet)); }; + +/** + * Get the hex code of a string colour for a colour used in pre dwv v0.17. + * + * @param {string} name The name of a colour. + * @returns {string} The hex representing the colour. + */ +dwv.utils.colourNameToHex = function (name) { + // default colours used in dwv version < 0.17 + var dict = { + Yellow: '#ffff00', + Red: '#ff0000', + White: '#ffffff', + Green: '#008000', + Blue: '#0000ff', + Lime: '#00ff00', + Fuchsia: '#ff00ff', + Black: '#000000' + }; + var res = '#ffff00'; + if (typeof dict[name] !== 'undefined') { + res = dict[name]; + } + return res; +}; diff --git a/src/utils/listen.js b/src/utils/listen.js index e4620d15c0..d9cf8a50d9 100644 --- a/src/utils/listen.js +++ b/src/utils/listen.js @@ -1,12 +1,12 @@ // namespaces var dwv = dwv || {}; -/** @namespace */ dwv.utils = dwv.utils || {}; /** * ListenerHandler class: handles add/removing and firing listeners. * * @class + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget#example */ dwv.utils.ListenerHandler = function () { /** @@ -62,9 +62,11 @@ dwv.utils.ListenerHandler = function () { if (typeof listeners[event.type] === 'undefined') { return; } - // fire events - for (var i = 0; i < listeners[event.type].length; ++i) { - listeners[event.type][i](event); + // fire events from a copy of the listeners array + // to avoid interference from possible add/remove + var stack = listeners[event.type].slice(); + for (var i = 0; i < stack.length; ++i) { + stack[i](event); } }; }; diff --git a/src/utils/logger.js b/src/utils/logger.js index e3b64ba688..ece1a6e9fd 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -2,11 +2,15 @@ var dwv = dwv || {}; /** @namespace */ dwv.utils = dwv.utils || {}; +/** @namespace */ dwv.utils.logger = dwv.utils.logger || {}; +/** @namespace */ dwv.utils.logger.console = dwv.utils.logger.console || {}; /** - * Main logger, defaults to the console logger. + * Main logger namespace. Defaults to the console logger. + * + * @see dwv.utils.logger.console */ dwv.logger = dwv.utils.logger.console; diff --git a/src/utils/string.js b/src/utils/string.js index 33fe6233ab..efcc84d839 100644 --- a/src/utils/string.js +++ b/src/utils/string.js @@ -185,17 +185,26 @@ dwv.utils.getRootPath = function (path) { }; /** - * Get a file extension + * Get a file extension: anything after the last dot. + * File name starting with a dot are discarded. + * Extensions are expected to contain at least one letter. * * @param {string} filePath The file path containing the file name. * @returns {string} The lower case file extension or null for none. */ dwv.utils.getFileExtension = function (filePath) { var ext = null; - if (typeof filePath !== 'undefined' && filePath) { - var pathSplit = filePath.split('.'); + if (typeof filePath !== 'undefined' && + filePath !== null && + filePath[0] !== '.') { + var pathSplit = filePath.toLowerCase().split('.'); if (pathSplit.length !== 1) { - ext = pathSplit.pop().toLowerCase(); + ext = pathSplit.pop(); + // extension should contain at least one letter and no slash + var regExp = /[a-z]/; + if (!regExp.test(ext) || ext.includes('/')) { + ext = null; + } } } return ext; diff --git a/src/utils/thread.js b/src/utils/thread.js index 46ec24efad..f9ce18e939 100644 --- a/src/utils/thread.js +++ b/src/utils/thread.js @@ -36,10 +36,10 @@ dwv.utils.ThreadPool = function (poolSize) { if (freeThreads.length > 0) { // get the first free worker thread var workerThread = freeThreads.shift(); - // run the input task - workerThread.run(workerTask); // add the thread to the runnning list runningThreads.push(workerThread); + // run the input task + workerThread.run(workerTask); } else { // no free thread, add task to queue taskQueue.push(workerTask); @@ -204,15 +204,15 @@ dwv.utils.WorkerThread = function (parentPool) { this.run = function (workerTask) { // store task runningTask = workerTask; - // create a new web worker - if (runningTask.script !== null) { + // create a new web worker if not done yet + if (typeof worker === 'undefined') { worker = new Worker(runningTask.script); // set callbacks worker.onmessage = onmessage; worker.onerror = onerror; - // launch the worker - worker.postMessage(runningTask.startMessage); } + // launch the worker + worker.postMessage(runningTask.startMessage); }; /** @@ -221,8 +221,6 @@ dwv.utils.WorkerThread = function (parentPool) { this.stop = function () { // stop the worker worker.terminate(); - // tell the parent pool this thread is free - parentPool.onTaskEnd(this); }; /** @@ -234,11 +232,14 @@ dwv.utils.WorkerThread = function (parentPool) { * @private */ function onmessage(event) { - // pass to parent - event.index = runningTask.id; + // augment event + event.itemNumber = runningTask.info.itemNumber; + event.numberOfItems = runningTask.info.numberOfItems; + event.dataIndex = runningTask.info.dataIndex; + // send event parentPool.onworkitem(event); - // stop the worker and free the thread - self.stop(); + // tell the parent pool the task is done + parentPool.onTaskEnd(self); } /** @@ -248,6 +249,10 @@ dwv.utils.WorkerThread = function (parentPool) { * @private */ function onerror(event) { + // augment event + event.itemNumber = runningTask.info.itemNumber; + event.numberOfItems = runningTask.info.numberOfItems; + event.dataIndex = runningTask.info.dataIndex; // pass to parent parentPool.handleWorkerError(event); // stop the worker and free the thread @@ -261,13 +266,13 @@ dwv.utils.WorkerThread = function (parentPool) { * @class * @param {string} script The worker script. * @param {object} message The data to pass to the worker. - * @param {number} index The worker id. + * @param {object} info Information object about the input data. */ -dwv.utils.WorkerTask = function (script, message, index) { +dwv.utils.WorkerTask = function (script, message, info) { // worker script this.script = script; // worker start message this.startMessage = message; - // worker id - this.id = index; + // information about the work data + this.info = info; }; diff --git a/src/utils/uri.js b/src/utils/uri.js index c2f2ab8fcd..c1b3db0386 100644 --- a/src/utils/uri.js +++ b/src/utils/uri.js @@ -312,7 +312,7 @@ dwv.utils.decodeManifest = function (manifest, nslices) { * Load from an input uri * * @param {string} uri The input uri, for example: 'window.location.href'. - * @param {object} app The associated app that handles the load. + * @param {dwv.App} app The associated app that handles the load. */ dwv.utils.loadFromUri = function (uri, app) { var query = dwv.utils.getUriQuery(uri); diff --git a/tests/bench/benchFunctionRunner.js b/tests/bench/benchFunctionRunner.js index 3ce6c817e4..a47706d3c7 100644 --- a/tests/bench/benchFunctionRunner.js +++ b/tests/bench/benchFunctionRunner.js @@ -49,7 +49,9 @@ dcmb.BenchFunctionRunner = function () { // avoid creating functions in loops var getFunc = function (f, a) { return function () { - f(a); + // run on a clone of the input array + // (in case it is modified...) + f(a.slice()); }; }; // add parsers to suite diff --git a/tests/dicom/dicomElementsWrapper.test.js b/tests/dicom/dicomElementsWrapper.test.js index 5b20ce1ebf..e8d9c367e8 100644 --- a/tests/dicom/dicomElementsWrapper.test.js +++ b/tests/dicom/dicomElementsWrapper.test.js @@ -1,9 +1,10 @@ /** * Tests for the 'dicom/dicomElementsWrapper.js' file. */ +/** @module tests/dicom */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('dicomElementsWrapper'); +QUnit.module('dicom'); /** * Tests for {@link dwv.dicom.DicomElementsWrapper} using simple DICOM data. diff --git a/tests/dicom/dicomGenerator.js b/tests/dicom/dicomGenerator.js index fdc909bd65..ed196e4bd9 100644 --- a/tests/dicom/dicomGenerator.js +++ b/tests/dicom/dicomGenerator.js @@ -33,8 +33,8 @@ dwv.dicom.getElementsFromJSONTags = function (tags) { offset += size; dicomElement.endOffset = offset; // create the tag group/element key - name = dwv.dicom.getGroupElementKey( - dicomElement.tag.group, dicomElement.tag.element); + name = new dwv.dicom.Tag( + dicomElement.tag.group, dicomElement.tag.element).getKey(); // store dicomElements[name] = dicomElement; } diff --git a/tests/dicom/dicomParser.test.js b/tests/dicom/dicomParser.test.js index 4d6ceabb26..597cafda19 100644 --- a/tests/dicom/dicomParser.test.js +++ b/tests/dicom/dicomParser.test.js @@ -1,10 +1,8 @@ /** * Tests for the 'dicom/dicomParser.js' file. */ -/** @module tests/dicom */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('dicomParser'); // WARNING about PhantomJS 1.9 // --------------------------- @@ -41,6 +39,8 @@ QUnit.test('Test simple DICOM parsing.', function (assert) { }; request.onload = function (/*event*/) { assert.ok((this.response.byteLength !== 0), 'Got a response.'); + assert.ok(dwv.dicom.hasDicomPrefix(this.response), + 'Response has DICOM prefix.'); // parse DICOM var dicomParser = new dwv.dicom.DicomParser(); @@ -107,6 +107,8 @@ QUnit.test('Test sequence DICOM parsing.', function (assert) { request.responseType = 'arraybuffer'; request.onload = function (/*event*/) { assert.ok((this.response.byteLength !== 0), 'Got a response.'); + assert.ok(dwv.dicom.hasDicomPrefix(this.response), + 'Response has DICOM prefix.'); // parse DICOM var dicomParser = new dwv.dicom.DicomParser(); diff --git a/tests/dicom/dicomWriter.test.js b/tests/dicom/dicomWriter.test.js index b66db00d2b..06ceb835d4 100644 --- a/tests/dicom/dicomWriter.test.js +++ b/tests/dicom/dicomWriter.test.js @@ -7,7 +7,6 @@ dwv.test = dwv.test || {}; */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('dicomWriter'); /** * Tests for {@link dwv.dicom.DicomWriter} using simple DICOM data. @@ -32,16 +31,21 @@ QUnit.test('Test multiframe writer support.', function (assert) { var dicomParser = new dwv.dicom.DicomParser(); dicomParser.parse(this.response); + var numCols = 256; + var numRows = 256; var numFrames = 16; + var bufferSize = numCols * numRows * numFrames; // raw tags var rawTags = dicomParser.getRawDicomElements(); // check values assert.equal(rawTags.x00280008.value[0], numFrames, 'Number of frames'); + assert.equal(rawTags.x00280011.value[0], numCols, 'Number of columns'); + assert.equal(rawTags.x00280010.value[0], numRows, 'Number of rows'); // length of value array for pixel data assert.equal( - rawTags.x7FE00010.value.length, - numFrames, + rawTags.x7FE00010.value[0].length, + bufferSize, 'Length of value array for pixel data'); var dicomWriter = new dwv.dicom.DicomWriter(); @@ -54,10 +58,12 @@ QUnit.test('Test multiframe writer support.', function (assert) { // check values assert.equal(rawTags.x00280008.value[0], numFrames, 'Number of frames'); + assert.equal(rawTags.x00280011.value[0], numCols, 'Number of columns'); + assert.equal(rawTags.x00280010.value[0], numRows, 'Number of rows'); // length of value array for pixel data assert.equal( - rawTags.x7FE00010.value.length, - numFrames, + rawTags.x7FE00010.value[0].length, + bufferSize, 'Length of value array for pixel data'); // finish async test @@ -199,28 +205,29 @@ dwv.test.compare = function (jsonTags, dicomElements, name, comparator) { } var keys = Object.keys(jsonTags); for (var k = 0; k < keys.length; ++k) { - var tag = keys[k]; - var tagGE = dwv.dicom.getGroupElementFromName(tag); - var tagKey = dwv.dicom.getGroupElementKey(tagGE.group, tagGE.element); + var tagName = keys[k]; + var tag = dwv.dicom.getTagFromDictionary(tagName); + var tagKey = tag.getKey(); var element = dicomElements.getDEFromKey(tagKey); var value = dicomElements.getFromKey(tagKey, true); if (element.vr !== 'SQ') { comparator.equal( dwv.test.toString(value), - jsonTags[tag], - name + ' - ' + tag); + jsonTags[tagName], + name + ' - ' + tagName); } else { // check content - if (jsonTags[tag] === null || jsonTags[tag] === 0) { + if (jsonTags[tagName] === null || jsonTags[tagName] === 0) { continue; } // supposing same order of subkeys and indices... - var subKeys = Object.keys(jsonTags[tag]); + var subKeys = Object.keys(jsonTags[tagName]); var index = 0; for (var sk = 0; sk < subKeys.length; ++sk) { if (subKeys[sk] !== 'explicitLength') { var wrap = new dwv.dicom.DicomElementsWrapper(value[index]); - dwv.test.compare(jsonTags[tag][subKeys[sk]], wrap, name, comparator); + dwv.test.compare( + jsonTags[tagName][subKeys[sk]], wrap, name, comparator); ++index; } } diff --git a/tests/dicom/filePixGenerator.js b/tests/dicom/filePixGenerator.js index 1d435ec716..0025ba8c3b 100644 --- a/tests/dicom/filePixGenerator.js +++ b/tests/dicom/filePixGenerator.js @@ -17,11 +17,11 @@ var FilePixGenerator = function (options) { for (var i = 0; i < imgs.length; ++i) { img = imgs[i]; if (img.width !== numberOfColumns) { - throw Error('Image width mismatch: ' + + throw new Error('Image width mismatch: ' + img.width + '!=' + numberOfColumns); } if (img.height !== numberOfRows) { - throw Error('Image height mismatch: ' + + throw new Error('Image height mismatch: ' + img.height + '!=' + numberOfRows); } } diff --git a/tests/dicom/mprPixGenerator.js b/tests/dicom/mprPixGenerator.js index fd736a604d..dfdbc9bc22 100644 --- a/tests/dicom/mprPixGenerator.js +++ b/tests/dicom/mprPixGenerator.js @@ -36,10 +36,12 @@ var MPRPixGenerator = function (options) { for (var i = 0; i < imgs.length; ++i) { img = imgs[i]; if (img.width !== halfNCols) { - throw Error('Image width mismatch: ' + img.width + '!=' + halfNCols); + throw new Error('Image width mismatch: ' + + img.width + '!=' + halfNCols); } if (img.height !== halfNRows) { - throw Error('Image height mismatch: ' + img.height + '!=' + halfNRows); + throw new Error('Image height mismatch: ' + + img.height + '!=' + halfNRows); } } // store @@ -53,8 +55,8 @@ var MPRPixGenerator = function (options) { this.generate = function (pixelBuffer, sliceNumber) { if (sliceNumber > numberOfSlices) { - throw Error('Cannot generate slice, number is above size: ' + - sliceNumber + ', ' + numberOfSlices); + throw new Error('Cannot generate slice, number is above size: ' + + sliceNumber + ', ' + numberOfSlices); } // axial var offset = 0; diff --git a/tests/dicom/pages/anonymiser.html b/tests/dicom/pages/anonymiser.html index c3a0a4f78f..2eb6e3c347 100644 --- a/tests/dicom/pages/anonymiser.html +++ b/tests/dicom/pages/anonymiser.html @@ -5,7 +5,10 @@ + + + diff --git a/tests/dicom/pages/generator.html b/tests/dicom/pages/generator.html index b6f75f45a0..36f68a6812 100644 --- a/tests/dicom/pages/generator.html +++ b/tests/dicom/pages/generator.html @@ -7,7 +7,9 @@ + + diff --git a/tests/dicom/pages/generator.js b/tests/dicom/pages/generator.js index f82c9d66b7..b994ee867a 100644 --- a/tests/dicom/pages/generator.js +++ b/tests/dicom/pages/generator.js @@ -89,6 +89,8 @@ function generateSlice(pixelGeneratorName, sliceNumber) { // image position var spacing = tags.PixelSpacing[0]; tags.ImagePositionPatient = [0, 0, sliceNumber * spacing]; + // instance number + tags.InstanceNumber = sliceNumber.toString(); // convert JSON to DICOM element object var res = dwv.dicom.getElementsFromJSONTags(tags); var dicomElements = res.elements; diff --git a/tests/dicom/pages/synthetic-data.html b/tests/dicom/pages/synthetic-data.html index 835d4b8e2f..6cab7f2f83 100644 --- a/tests/dicom/pages/synthetic-data.html +++ b/tests/dicom/pages/synthetic-data.html @@ -8,7 +8,9 @@ + + diff --git a/tests/gui/layerGroup.test.js b/tests/gui/layerGroup.test.js new file mode 100644 index 0000000000..92c48162e7 --- /dev/null +++ b/tests/gui/layerGroup.test.js @@ -0,0 +1,36 @@ +/** + * Tests for the 'gui/LayerGroup.js' file. + */ +// Do not warn if these variables were not defined before. +/* global QUnit */ + +/** + * Tests for {@link dwv.gui.LayerGroup} string id. + * + * @function module:tests/gui~LayerGroup + */ +QUnit.test('Test LayerGroup string id.', function (assert) { + // test #00 + var theoId00 = 'layer-0-0'; + var theoDetails00 = {groupId: 0, layerId: 0}; + var id00 = dwv.gui.getLayerGroupDivId( + theoDetails00.groupId, theoDetails00.layerId); + var details00 = dwv.gui.getLayerDetailsFromLayerDivId(theoId00); + assert.equal(id00, theoId00, 'getLayerGroupDivId #00'); + assert.equal(details00.groupId, theoDetails00.groupId, + 'getLayerDetailsFromLayerDivId groupId #00'); + assert.equal(details00.layerId, theoDetails00.layerId, + 'getLayerDetailsFromLayerDivId layerId #00'); + + // test #01 + var theoId01 = 'layer-1-2'; + var theoDetails01 = {groupId: 1, layerId: 2}; + var id01 = dwv.gui.getLayerGroupDivId( + theoDetails01.groupId, theoDetails01.layerId); + var details01 = dwv.gui.getLayerDetailsFromLayerDivId(theoId01); + assert.equal(id01, theoId01, 'getLayerGroupDivId #01'); + assert.equal(details01.groupId, theoDetails01.groupId, + 'getLayerDetailsFromLayerDivId groupId #01'); + assert.equal(details01.layerId, theoDetails01.layerId, + 'getLayerDetailsFromLayerDivId layerId #01'); +}); diff --git a/tests/image/geometry.test.js b/tests/image/geometry.test.js index ea330bbfe2..48d3ebfdad 100644 --- a/tests/image/geometry.test.js +++ b/tests/image/geometry.test.js @@ -4,53 +4,47 @@ /** @module tests/image */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('geometry'); +QUnit.module('image'); /** - * Tests for {@link dwv.image.Size}. + * Tests for {@link dwv.image.Geometry}. * - * @function module:tests/image~size + * @function module:tests/image~geometry */ -QUnit.test('Test Size.', function (assert) { - var size0 = new dwv.image.Size(2, 3, 4); - // test its values - assert.equal(size0.getNumberOfColumns(), 2, 'getNumberOfColumns'); - assert.equal(size0.getNumberOfRows(), 3, 'getNumberOfRows'); - assert.equal(size0.getNumberOfSlices(), 4, 'getNumberOfSlices'); - assert.equal(size0.getSliceSize(), 6, 'getSliceSize'); - assert.equal(size0.getTotalSize(), 24, 'getTotalSize'); - // defaults - var size00 = new dwv.image.Size(2, 3); - assert.equal(size00.getNumberOfSlices(), 1, 'getNumberOfSlices default'); - // equality - assert.equal(size0.equals(size0), 1, 'equals self true'); - var size1 = new dwv.image.Size(2, 3, 4); - assert.equal(size0.equals(size1), 1, 'equals true'); - var size2 = new dwv.image.Size(3, 3, 4); - assert.equal(size0.equals(size2), 0, 'equals false'); - // is in bounds - assert.equal(size0.isInBounds(0, 0, 0), 1, 'isInBounds 0,0,0'); - assert.equal(size0.isInBounds(0, 0), 1, 'isInBounds 0,0'); - assert.equal(size0.isInBounds(1, 2, 3), 1, 'isInBounds max'); - assert.equal(size0.isInBounds(2, 3, 4), 0, 'isInBounds too big'); - assert.equal(size0.isInBounds(-1, 2, 3), 0, 'isInBounds too small'); -}); +QUnit.test('Test Geometry.', function (assert) { + var size0 = 4; + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); + var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); + var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); -/** - * Tests for {@link dwv.image.Spacing}. - * - * @function module:tests/image~spacing - */ -QUnit.test('Test Spacing.', function (assert) { - var spacing0 = new dwv.image.Spacing(2, 3, 4); - // test its values - assert.equal(spacing0.getColumnSpacing(), 2, 'getColumnSpacing'); - assert.equal(spacing0.getRowSpacing(), 3, 'getRowSpacing'); - assert.equal(spacing0.getSliceSpacing(), 4, 'getSliceSpacing'); - // equality - assert.equal(spacing0.equals(spacing0), 1, 'equals self true'); - var spacing1 = new dwv.image.Spacing(2, 3, 4); - assert.equal(spacing0.equals(spacing1), 1, 'equals true'); - var spacing2 = new dwv.image.Spacing(3, 3, 4); - assert.equal(spacing0.equals(spacing2), 0, 'equals false'); + var testData = [ + {vals: [0, 0, 0], offset: 0}, + {vals: [1, 0, 0], offset: 1}, + {vals: [2, 0, 0], offset: 2}, + {vals: [3, 0, 0], offset: 3}, + {vals: [0, 1, 0], offset: 4}, + {vals: [1, 1, 0], offset: 5}, + {vals: [2, 1, 0], offset: 6}, + {vals: [3, 1, 0], offset: 7}, + {vals: [0, 2, 0], offset: 8}, + {vals: [1, 2, 0], offset: 9}, + {vals: [2, 2, 0], offset: 10}, + {vals: [3, 2, 0], offset: 11}, + {vals: [0, 3, 0], offset: 12}, + {vals: [1, 3, 0], offset: 13}, + {vals: [2, 3, 0], offset: 14}, + {vals: [3, 3, 0], offset: 15} + ]; + for (var i = 0; i < testData.length; ++i) { + var index = new dwv.math.Index(testData[i].vals); + + var theoPoint = new dwv.math.Point([ + testData[i].vals[0], testData[i].vals[1], testData[i].vals[2] + ]); + var resPoint = imgGeometry0.indexToWorld(index); + assert.true(theoPoint.equals(resPoint), 'indexToWorkd #' + i); + var resPoint2 = imgGeometry0.worldToIndex(theoPoint); + assert.true(index.equals(resPoint2), 'worldToIndex #' + i); + } }); diff --git a/tests/image/image.test.js b/tests/image/image.test.js index d769321ee7..0951c61c3f 100644 --- a/tests/image/image.test.js +++ b/tests/image/image.test.js @@ -7,7 +7,6 @@ dwv.test = dwv.test || {}; */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('image'); /** * Compare an image and a buffer. @@ -24,9 +23,9 @@ dwv.test.compareImageAndBuffer = function (image, size, buffer, rsi) { // calculate differences var index = 0; - for (var k = 0; k < size.getNumberOfSlices(); ++k) { - for (var j = 0; j < size.getNumberOfRows(); ++j) { - for (var i = 0; i < size.getNumberOfColumns(); ++i) { + for (var k = 0; k < size.get(2); ++k) { + for (var j = 0; j < size.get(1); ++j) { + for (var i = 0; i < size.get(0); ++i) { var diff = Math.abs(image.getValue(i, j, k) - buffer[index]); if (diff !== 0) { diffs.push(diff); @@ -68,15 +67,15 @@ QUnit.test('Test Image getValue.', function (assert) { // create a simple image var size0 = 4; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); // test its geometry assert.equal(image0.getGeometry(), imgGeometry0, 'Image geometry'); // test its values @@ -104,11 +103,11 @@ QUnit.test('Test Image getValue.', function (assert) { assert.equal(imgRange01.min, theoRange0.min, 'Rescaled range min'); // image with rescale - var image1 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image1 = new dwv.image.Image(imgGeometry0, buffer0); var slope1 = 2; var intercept1 = 10; var rsi1 = new dwv.image.RescaleSlopeAndIntercept(slope1, intercept1); - image1.setRescaleSlopeAndIntercept(rsi1); + image1.setRescaleSlopeAndIntercept(rsi1, new dwv.math.Index([0, 0, 0])); // test its geometry assert.equal(image1.getGeometry(), imgGeometry0, 'Image geometry'); // test its values @@ -142,15 +141,15 @@ QUnit.test('Test Image getValue.', function (assert) { QUnit.test('Test Image histogram.', function (assert) { // create a simple image var size0 = 4; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); // histogram var histogram = image0.getHistogram(); @@ -175,17 +174,30 @@ QUnit.test('Test Image histogram.', function (assert) { * @function module:tests/image~append */ QUnit.test('Test Image append slice.', function (assert) { + /** + * Compare two arrays of vectors. + * + * @param {Array} arr0 The first array. + * @param {Array} arr1 The second array. + * @returns {boolean} True if both arrays are equal. + */ + function compareArrayOfVectors(arr0, arr1) { + return arr0.every(function (element, index) { + return element.equals(arr1[index]); + }); + } + var size = 4; - var imgSize = new dwv.image.Size(size, size, 2); - var imgSizeMinusOne = new dwv.image.Size(size, size, 1); - var imgSpacing = new dwv.image.Spacing(1, 1, 1); - var imgOrigin = new dwv.math.Point3D(0, 0, 0); + var imgSize = new dwv.image.Size([size, size, 2]); + var imgSizeMinusOne = new dwv.image.Size([size, size, 1]); + var imgSpacing = new dwv.image.Spacing([1, 1, 1]); + var imgOrigin = new dwv.math.Point3D(0, 0, 1); var imgGeometry0 = new dwv.image.Geometry( imgOrigin, imgSizeMinusOne, imgSpacing); - imgGeometry0.appendOrigin(new dwv.math.Point3D(0, 0, 1), 1); + imgGeometry0.appendOrigin(new dwv.math.Point3D(0, 0, 0), 1); // slice to append - var sliceSize = new dwv.image.Size(size, size, 1); + var sliceSize = new dwv.image.Size([size, size, 1]); var sliceBuffer = new Int16Array(sliceSize.getTotalSize()); for (var i = 0; i < size * size; ++i) { sliceBuffer[i] = 2; @@ -194,96 +206,99 @@ QUnit.test('Test Image append slice.', function (assert) { // image buffer var buffer = new Int16Array(imgSize.getTotalSize()); for (var j = 0; j < size * size; ++j) { - buffer[j] = 0; + buffer[j] = 1; } for (var k = size * size; k < 2 * size * size; ++k) { - buffer[k] = 1; + buffer[k] = 0; } // image 0 - var image0 = new dwv.image.Image(imgGeometry0, [buffer], 1, ['0']); + var image0 = new dwv.image.Image(imgGeometry0, buffer, ['0']); + image0.setMeta({numberOfFiles: 3}); // append null assert.throws(function () { image0.appendSlice(null); }, new Error('Cannot append null slice'), 'append null slice'); // real slice - var sliceOrigin = new dwv.math.Point3D(0, 0, -1); + var sliceOrigin = new dwv.math.Point3D(0, 0, 2); var sliceGeometry = new dwv.image.Geometry( sliceOrigin, sliceSize, imgSpacing); - var slice0 = new dwv.image.Image(sliceGeometry, [sliceBuffer], 1, ['1']); + var slice0 = new dwv.image.Image(sliceGeometry, sliceBuffer, ['1']); + slice0.setMeta({numberOfFiles: 3}); // append slice before image0.appendSlice(slice0); // test its values assert.equal(image0.getValue(0, 0, 0), 2, 'Value at 0,0,0 (append before)'); assert.equal(image0.getValue(3, 3, 0), 2, 'Value at 3,3,0 (append before)'); - assert.equal(image0.getValue(0, 0, 1), 0, 'Value at 0,0,1 (append before)'); - assert.equal(image0.getValue(3, 3, 1), 0, 'Value at 3,3,1 (append before)'); - assert.equal(image0.getValue(0, 0, 2), 1, 'Value at 0,0,2 (append before)'); - assert.equal(image0.getValue(3, 3, 2), 1, 'Value at 3,3,2 (append before)'); + assert.equal(image0.getValue(0, 0, 1), 1, 'Value at 0,0,1 (append before)'); + assert.equal(image0.getValue(3, 3, 1), 1, 'Value at 3,3,1 (append before)'); + assert.equal(image0.getValue(0, 0, 2), 0, 'Value at 0,0,2 (append before)'); + assert.equal(image0.getValue(3, 3, 2), 0, 'Value at 3,3,2 (append before)'); // test its positions var sliceOrigins0 = []; - sliceOrigins0[0] = new dwv.math.Point3D(0, 0, -1); - sliceOrigins0[1] = new dwv.math.Point3D(0, 0, 0); - sliceOrigins0[2] = new dwv.math.Point3D(0, 0, 1); - assert.deepEqual( - imgGeometry0.getOrigins(), - sliceOrigins0, + sliceOrigins0[0] = new dwv.math.Point3D(0, 0, 2); + sliceOrigins0[1] = new dwv.math.Point3D(0, 0, 1); + sliceOrigins0[2] = new dwv.math.Point3D(0, 0, 0); + assert.ok( + compareArrayOfVectors(imgGeometry0.getOrigins(), sliceOrigins0), 'Slice positions (append before)'); // image 1 var imgGeometry1 = new dwv.image.Geometry( imgOrigin, imgSizeMinusOne, imgSpacing); - imgGeometry1.appendOrigin(new dwv.math.Point3D(0, 0, 1), 1); - var image1 = new dwv.image.Image(imgGeometry1, [buffer], 1, ['0']); - var sliceOrigin1 = new dwv.math.Point3D(0, 0, 2); + imgGeometry1.appendOrigin(new dwv.math.Point3D(0, 0, 0), 1); + var image1 = new dwv.image.Image(imgGeometry1, buffer, ['0']); + image1.setMeta({numberOfFiles: 3}); + var sliceOrigin1 = new dwv.math.Point3D(0, 0, -1); var sliceGeometry1 = new dwv.image.Geometry( sliceOrigin1, sliceSize, imgSpacing); - var slice1 = new dwv.image.Image(sliceGeometry1, [sliceBuffer], 1, ['0']); + var slice1 = new dwv.image.Image(sliceGeometry1, sliceBuffer, ['0']); + slice1.setMeta({numberOfFiles: 3}); // append slice before image1.appendSlice(slice1); // test its values - assert.equal(image1.getValue(0, 0, 0), 0, 'Value at 0,0,0 (append after)'); - assert.equal(image1.getValue(3, 3, 0), 0, 'Value at 3,3,0 (append after)'); - assert.equal(image1.getValue(0, 0, 1), 1, 'Value at 0,0,1 (append after)'); - assert.equal(image1.getValue(3, 3, 1), 1, 'Value at 3,3,1 (append after)'); + assert.equal(image1.getValue(0, 0, 0), 1, 'Value at 0,0,0 (append after)'); + assert.equal(image1.getValue(3, 3, 0), 1, 'Value at 3,3,0 (append after)'); + assert.equal(image1.getValue(0, 0, 1), 0, 'Value at 0,0,1 (append after)'); + assert.equal(image1.getValue(3, 3, 1), 0, 'Value at 3,3,1 (append after)'); assert.equal(image1.getValue(0, 0, 2), 2, 'Value at 0,0,2 (append after)'); assert.equal(image1.getValue(3, 3, 2), 2, 'Value at 3,3,2 (append after)'); // test its positions var sliceOrigins1 = []; - sliceOrigins1[0] = new dwv.math.Point3D(0, 0, 0); - sliceOrigins1[1] = new dwv.math.Point3D(0, 0, 1); - sliceOrigins1[2] = new dwv.math.Point3D(0, 0, 2); - assert.deepEqual( - imgGeometry1.getOrigins(), - sliceOrigins1, + sliceOrigins1[0] = new dwv.math.Point3D(0, 0, 1); + sliceOrigins1[1] = new dwv.math.Point3D(0, 0, 0); + sliceOrigins1[2] = new dwv.math.Point3D(0, 0, -1); + assert.ok( + compareArrayOfVectors(imgGeometry1.getOrigins(), sliceOrigins1), 'Slice positions (append after)'); // image 2 var imgGeometry2 = new dwv.image.Geometry( imgOrigin, imgSizeMinusOne, imgSpacing); - imgGeometry2.appendOrigin(new dwv.math.Point3D(0, 0, 1), 1); - var image2 = new dwv.image.Image(imgGeometry2, [buffer], 1, ['0']); + imgGeometry2.appendOrigin(new dwv.math.Point3D(0, 0, 0), 1); + var image2 = new dwv.image.Image(imgGeometry2, buffer, ['0']); + image2.setMeta({numberOfFiles: 3}); var sliceOrigin2 = new dwv.math.Point3D(0, 0, 0.4); var sliceGeometry2 = new dwv.image.Geometry( sliceOrigin2, sliceSize, imgSpacing); - var slice2 = new dwv.image.Image(sliceGeometry2, [sliceBuffer], 1, ['0']); + var slice2 = new dwv.image.Image(sliceGeometry2, sliceBuffer, ['0']); + slice2.setMeta({numberOfFiles: 3}); // append slice before image2.appendSlice(slice2); // test its values - assert.equal(image2.getValue(0, 0, 0), 0, 'Value at 0,0,0 (append between)'); - assert.equal(image2.getValue(3, 3, 0), 0, 'Value at 3,3,0 (append between)'); + assert.equal(image2.getValue(0, 0, 0), 1, 'Value at 0,0,0 (append between)'); + assert.equal(image2.getValue(3, 3, 0), 1, 'Value at 3,3,0 (append between)'); assert.equal(image2.getValue(0, 0, 1), 2, 'Value at 0,0,1 (append between)'); assert.equal(image2.getValue(3, 3, 1), 2, 'Value at 3,3,1 (append between)'); - assert.equal(image2.getValue(0, 0, 2), 1, 'Value at 0,0,2 (append between)'); - assert.equal(image2.getValue(3, 3, 2), 1, 'Value at 3,3,2 (append between)'); + assert.equal(image2.getValue(0, 0, 2), 0, 'Value at 0,0,2 (append between)'); + assert.equal(image2.getValue(3, 3, 2), 0, 'Value at 3,3,2 (append between)'); // test its positions var sliceOrigins2 = []; - sliceOrigins2[0] = new dwv.math.Point3D(0, 0, 0); + sliceOrigins2[0] = new dwv.math.Point3D(0, 0, 1); sliceOrigins2[1] = new dwv.math.Point3D(0, 0, 0.4); - sliceOrigins2[2] = new dwv.math.Point3D(0, 0, 1); - assert.deepEqual( - imgGeometry2.getOrigins(), - sliceOrigins2, + sliceOrigins2[2] = new dwv.math.Point3D(0, 0, 0); + assert.ok( + compareArrayOfVectors(imgGeometry2.getOrigins(), sliceOrigins2), 'Slice positions (append between)'); }); @@ -295,15 +310,15 @@ QUnit.test('Test Image append slice.', function (assert) { QUnit.test('Test Image convolute2D.', function (assert) { // create a simple image var size0 = 3; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); // id convolution var weights0 = [0, 0, 0, 0, 1, 0, 0, 0, 0]; var resImage0 = image0.convolute2D(weights0); @@ -337,15 +352,15 @@ QUnit.test('Test Image convolute2D.', function (assert) { QUnit.test('Test Image transform.', function (assert) { // create a simple image var size0 = 3; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); // treshold function var func0 = function (value) { @@ -367,7 +382,7 @@ QUnit.test('Test Image transform.', function (assert) { assert.equal(testContent0, true, 'transform threshold'); // new image - image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + image0 = new dwv.image.Image(imgGeometry0, buffer0); // multiply function var func1 = function (value) { @@ -393,20 +408,20 @@ QUnit.test('Test Image transform.', function (assert) { QUnit.test('Test Image compose.', function (assert) { // create two simple images var size0 = 3; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); var buffer1 = []; for (i = 0; i < size0 * size0; ++i) { buffer1[i] = i; } - var image1 = new dwv.image.Image(imgGeometry0, [buffer1]); + var image1 = new dwv.image.Image(imgGeometry0, buffer1); // addition function var func0 = function (a, b) { @@ -433,8 +448,8 @@ QUnit.test('Test ImageFactory.', function (assert) { var zeroStats = new dwv.math.SimpleStats(0, 0, 0, 0); var size0 = 3; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; @@ -445,12 +460,12 @@ QUnit.test('Test ImageFactory.', function (assert) { var dicomElements0 = []; // columns - dicomElements0.x00280011 = {value: imgSize0.getNumberOfColumns()}; + dicomElements0.x00280011 = {value: imgSize0.get(0)}; // rows - dicomElements0.x00280010 = {value: imgSize0.getNumberOfRows()}; + dicomElements0.x00280010 = {value: imgSize0.get(1)}; // spacing dicomElements0.x00280030 = { - value: [imgSpacing0.getRowSpacing(), imgSpacing0.getColumnSpacing()] + value: [imgSpacing0.get(1), imgSpacing0.get(2)] }; // transfer syntax (explicit VR) dicomElements0.x00020010 = {value: '1.2.840.10008.1.2.1'}; @@ -461,7 +476,7 @@ QUnit.test('Test ImageFactory.', function (assert) { // create the image factory var factory0 = new dwv.image.ImageFactory(); // create the image - var image0 = factory0.create(wrappedDicomElements0, [buffer0]); + var image0 = factory0.create(wrappedDicomElements0, buffer0); // test its geometry assert.ok(image0.getGeometry().equals(imgGeometry0), 'Image geometry'); diff --git a/tests/image/iterator.test.js b/tests/image/iterator.test.js index 3341bc2ba1..47f782b2be 100644 --- a/tests/image/iterator.test.js +++ b/tests/image/iterator.test.js @@ -1,17 +1,190 @@ +// namespace +var dwv = dwv || {}; +dwv.test = dwv.test || {}; +dwv.test.data = dwv.test.data || {}; + /** * Tests for the 'image/iterator.js' file. */ -/** @module tests/image */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('image'); + +/* eslint-disable array-element-newline */ +dwv.test.data.iterator0 = { + ncols: 3, + nrows: 2, + nslices: 4, + buffer: [ + 0, 1, 2, + 3, 4, 5, + 10, 11, 12, + 13, 14, 15, + 20, 21, 22, + 23, 24, 25, + 30, 31, 32, + 33, 34, 35 + ], + valuesAx: [ + [0, 1, 2, 3, 4, 5], + [10, 11, 12, 13, 14, 15], + [20, 21, 22, 23, 24, 25], + [30, 31, 32, 33, 34, 35] + ], + valuesAxR1: [ + [5, 4, 3, 2, 1, 0], + [15, 14, 13, 12, 11, 10], + [25, 24, 23, 22, 21, 20], + [35, 34, 33, 32, 31, 30] + ], + valuesAxR2: [ + [2, 1, 0, 5, 4, 3], + [12, 11, 10, 15, 14, 13], + [22, 21, 20, 25, 24, 23], + [32, 31, 30, 35, 34, 33] + ], + valuesAxR1R2: [ + [3, 4, 5, 0, 1, 2], + [13, 14, 15, 10, 11, 12], + [23, 24, 25, 20, 21, 22], + [33, 34, 35, 30, 31, 32] + ], + valuesAx2: [ + [0, 3, 1, 4, 2, 5], + [10, 13, 11, 14, 12, 15], + [20, 23, 21, 24, 22, 25], + [30, 33, 31, 34, 32, 35] + ], + valuesAx2R1: [ + [5, 2, 4, 1, 3, 0], + [15, 12, 14, 11, 13, 10], + [25, 22, 24, 21, 23, 20], + [35, 32, 34, 31, 33, 30] + ], + valuesAx2R2: [ + [3, 0, 4, 1, 5, 2], + [13, 10, 14, 11, 15, 12], + [23, 20, 24, 21, 25, 22], + [33, 30, 34, 31, 35, 32] + ], + valuesAx2R1R2: [ + [2, 5, 1, 4, 0, 3], + [12, 15, 11, 14, 10, 13], + [22, 25, 21, 24, 20, 23], + [32, 35, 31, 34, 30, 33] + ], + valuesCo: [ + [0, 1, 2, 10, 11, 12, 20, 21, 22, 30, 31, 32], + [3, 4, 5, 13, 14, 15, 23, 24, 25, 33, 34, 35] + ], + valuesCoR1: [ + [32, 31, 30, 22, 21, 20, 12, 11, 10, 2, 1, 0], + [35, 34, 33, 25, 24, 23, 15, 14, 13, 5, 4, 3] + ], + valuesCoR2: [ + [2, 1, 0, 12, 11, 10, 22, 21, 20, 32, 31, 30], + [5, 4, 3, 15, 14, 13, 25, 24, 23, 35, 34, 33] + ], + valuesCoR1R2: [ + [30, 31, 32, 20, 21, 22, 10, 11, 12, 0, 1, 2], + [33, 34, 35, 23, 24, 25, 13, 14, 15, 3, 4, 5] + ], + valuesCo2: [ + [0, 10, 20, 30, 1, 11, 21, 31, 2, 12, 22, 32], + [3, 13, 23, 33, 4, 14, 24, 34, 5, 15, 25, 35] + ], + valuesCo2R1: [ + [32, 22, 12, 2, 31, 21, 11, 1, 30, 20, 10, 0], + [35, 25, 15, 5, 34, 24, 14, 4, 33, 23, 13, 3] + ], + valuesCo2R2: [ + [30, 20, 10, 0, 31, 21, 11, 1, 32, 22, 12, 2], + [33, 23, 13, 3, 34, 24, 14, 4, 35, 25, 15, 5] + ], + valuesCo2R1R2: [ + [2, 12, 22, 32, 1, 11, 21, 31, 0, 10, 20, 30], + [5, 15, 25, 35, 4, 14, 24, 34, 3, 13, 23, 33] + ], + valuesSa: [ + [0, 3, 10, 13, 20, 23, 30, 33], + [1, 4, 11, 14, 21, 24, 31, 34], + [2, 5, 12, 15, 22, 25, 32, 35] + ], + valuesSaR1: [ + [33, 30, 23, 20, 13, 10, 3, 0], + [34, 31, 24, 21, 14, 11, 4, 1], + [35, 32, 25, 22, 15, 12, 5, 2] + ], + valuesSaR2: [ + [3, 0, 13, 10, 23, 20, 33, 30], + [4, 1, 14, 11, 24, 21, 34, 31], + [5, 2, 15, 12, 25, 22, 35, 32] + ], + valuesSaR1R2: [ + [30, 33, 20, 23, 10, 13, 0, 3], + [31, 34, 21, 24, 11, 14, 1, 4], + [32, 35, 22, 25, 12, 15, 2, 5] + ], + valuesSa2: [ + [0, 10, 20, 30, 3, 13, 23, 33], + [1, 11, 21, 31, 4, 14, 24, 34], + [2, 12, 22, 32, 5, 15, 25, 35] + ], + valuesSa2R1: [ + [33, 23, 13, 3, 30, 20, 10, 0], + [34, 24, 14, 4, 31, 21, 11, 1], + [35, 25, 15, 5, 32, 22, 12, 2] + ], + valuesSa2R2: [ + [30, 20, 10, 0, 33, 23, 13, 3], + [31, 21, 11, 1, 34, 24, 14, 4], + [32, 22, 12, 2, 35, 25, 15, 5] + ], + valuesSa2R1R2: [ + [3, 13, 23, 33, 0, 10, 20, 30], + [4, 14, 24, 34, 1, 11, 21, 31], + [5, 15, 25, 35, 2, 12, 22, 32] + ] +}; +/* eslint-enable array-element-newline */ /** - * Tests for {@link dwv.image.range}. + * Run an input iterator and store values. * - * @function module:tests/image~range + * @param {object} iter The iterator. + * @returns {Array} The result array. + */ +dwv.test.runIterator = function (iter) { + var res = []; + var ival = iter.next(); + while (!ival.done) { + res.push(ival.value); + ival = iter.next(); + } + return res; +}; + +/** + * Check iter. + * + * @param {object} assert The qunit assert. + * @param {Function} getIter Function to get the iter at a given position. + * @param {Array} theoValues Theoretical values. + * @param {string} name String to identify test. */ -QUnit.test('Test iterator.', function (assert) { +dwv.test.checkIterator = function (assert, getIter, theoValues, name) { + for (var i = 0; i < theoValues.length; ++i) { + var res = dwv.test.runIterator(getIter(i)); + var theo = theoValues[i]; + assert.deepEqual(res, theo, 'range ' + name + ' #' + i); + } +}; + +/** + * Tests for {@link dwv.image.simpleRange}. + * + * @function module:tests/image~simpleRange + */ +QUnit.test('Test simpleRange iterator.', function (assert) { var dataAccessor = function (offset) { return offset; }; @@ -19,7 +192,7 @@ QUnit.test('Test iterator.', function (assert) { var test0Min = 0; var test0Max = 10; var i0Theo = test0Min; - var iter0 = dwv.image.range(dataAccessor, test0Min, test0Max); + var iter0 = dwv.image.simpleRange(dataAccessor, test0Min, test0Max); var ival0 = iter0.next(); while (!ival0.done) { assert.equal(ival0.value, i0Theo, '#0 iterator next'); @@ -33,7 +206,8 @@ QUnit.test('Test iterator.', function (assert) { var test1Max = 21; var test1Incr = 2; var i1Theo = test1Min; - var iter1 = dwv.image.range(dataAccessor, test1Min, test1Max, test1Incr); + var iter1 = dwv.image.simpleRange( + dataAccessor, test1Min, test1Max, test1Incr); var ival1 = iter1.next(); while (!ival1.done) { assert.equal(ival1.value, i1Theo, '#1 iterator next'); @@ -43,6 +217,182 @@ QUnit.test('Test iterator.', function (assert) { assert.equal(test1Max, i1Theo, '#1 iterator max'); }); +/** + * Tests for {@link dwv.image.range}. + * + * @function module:tests/image~range + */ +QUnit.test('Test range iterator: axial', function (assert) { + // test data + var testData0 = dwv.test.data.iterator0; + var ncols = testData0.ncols; + var nrows = testData0.nrows; + var sliceSize = ncols * nrows; + var dataAccessor = function (offset) { + return testData0.buffer[offset]; + }; + + // axial: xyz + var getAxIter = function (reverse1, reverse2) { + return function (index) { + var min = index * sliceSize; + var max = min + sliceSize; + var start = reverse1 ? max - 1 : min; + var maxIter = sliceSize; + return dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, ncols, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getAxIter(false, false), testData0.valuesAx, 'axial'); + dwv.test.checkIterator(assert, + getAxIter(true, false), testData0.valuesAxR1, 'axialR1'); + dwv.test.checkIterator(assert, + getAxIter(false, true), testData0.valuesAxR2, 'axialR2'); + dwv.test.checkIterator(assert, + getAxIter(true, true), testData0.valuesAxR1R2, 'axialR1R2'); + + // axial: yxz + var getAx2Iter = function (reverse1, reverse2) { + return function (index) { + var min = index * sliceSize; + var max = min + sliceSize; + var start = reverse1 ? max - 1 : min; + var maxIter = sliceSize; + return dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, 1, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getAx2Iter(false, false), testData0.valuesAx2, 'axial2'); + dwv.test.checkIterator(assert, + getAx2Iter(true, false), testData0.valuesAx2R1, 'axial2R1'); + dwv.test.checkIterator(assert, + getAx2Iter(false, true), testData0.valuesAx2R2, 'axial2R2'); + dwv.test.checkIterator(assert, + getAx2Iter(true, true), testData0.valuesAx2R1R2, 'axial2R1R2'); +}); + +/** + * Tests for {@link dwv.image.range}. + * + * @function module:tests/image~range + */ +QUnit.test('Test range iterator: coronal', function (assert) { + // test data + var testData0 = dwv.test.data.iterator0; + var ncols = testData0.ncols; + var nrows = testData0.nrows; + var nslices = testData0.nslices; + var sliceSize = ncols * nrows; + var dataAccessor = function (offset) { + return testData0.buffer[offset]; + }; + + // coronal: xzy + var getCoroIter = function (reverse1, reverse2) { + return function (index) { + var min = index * ncols; + var max = min + (nslices - 1) * sliceSize + ncols; + var start = reverse1 ? max - 1 : min; + var maxIter = nslices * ncols; + return dwv.image.range(dataAccessor, + start, maxIter, 1, ncols, sliceSize, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getCoroIter(false, false), testData0.valuesCo, 'coronal'); + dwv.test.checkIterator(assert, + getCoroIter(true, false), testData0.valuesCoR1, 'coronalR1'); + dwv.test.checkIterator(assert, + getCoroIter(false, true), testData0.valuesCoR2, 'coronalR2'); + dwv.test.checkIterator(assert, + getCoroIter(true, true), testData0.valuesCoR1R2, 'coronalR1R2'); + + // coronal: zxy + var getCoro2Iter = function (reverse1, reverse2) { + return function (index) { + var min = index * ncols; + var max = min + (nslices - 1) * sliceSize + ncols; + var start = reverse1 ? max - 1 : min; + var maxIter = nslices * ncols; + return dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, 1, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getCoro2Iter(false, false), testData0.valuesCo2, 'coronal2'); + dwv.test.checkIterator(assert, + getCoro2Iter(true, false), testData0.valuesCo2R1, 'coronal2R1'); + dwv.test.checkIterator(assert, + getCoro2Iter(false, true), testData0.valuesCo2R2, 'coronal2R2'); + dwv.test.checkIterator(assert, + getCoro2Iter(true, true), testData0.valuesCo2R1R2, 'coronal2R1R2'); +}); + +/** + * Tests for {@link dwv.image.range}. + * + * @function module:tests/image~range + */ +QUnit.test('Test range iterator: sagittal', function (assert) { + // test data + var testData0 = dwv.test.data.iterator0; + var ncols = testData0.ncols; + var nrows = testData0.nrows; + var nslices = testData0.nslices; + var sliceSize = ncols * nrows; + var dataAccessor = function (offset) { + return testData0.buffer[offset]; + }; + + // sagittal: yzx + var getSagIter = function (reverse1, reverse2) { + return function (index) { + var min = index; + var max = min + (nslices - 1) * sliceSize + ncols * (nrows - 1); + var start = reverse1 ? max : min; + var maxIter = nslices * nrows; + return dwv.image.range(dataAccessor, + start, maxIter, ncols, nrows, sliceSize, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getSagIter(false, false), testData0.valuesSa, 'sagittal'); + dwv.test.checkIterator(assert, + getSagIter(true, false), testData0.valuesSaR1, 'sagittalR1'); + dwv.test.checkIterator(assert, + getSagIter(false, true), testData0.valuesSaR2, 'sagittalR2'); + dwv.test.checkIterator(assert, + getSagIter(true, true), testData0.valuesSaR1R2, 'sagittalR1R2'); + + // sagittal: zyx + var getSag2Iter = function (reverse1, reverse2) { + return function (index) { + var min = index; + var max = min + (nslices - 1) * sliceSize + ncols * (nrows - 1); + var start = reverse1 ? max : min; + var maxIter = nslices * nrows; + return dwv.image.range(dataAccessor, + start, maxIter, sliceSize, nslices, ncols, reverse1, reverse2); + }; + }; + + dwv.test.checkIterator(assert, + getSag2Iter(false, false), testData0.valuesSa2, 'sagittal2'); + dwv.test.checkIterator(assert, + getSag2Iter(true, false), testData0.valuesSa2R1, 'sagittal2R1'); + dwv.test.checkIterator(assert, + getSag2Iter(false, true), testData0.valuesSa2R2, 'sagittal2R2'); + dwv.test.checkIterator(assert, + getSag2Iter(true, true), testData0.valuesSa2R1R2, 'sagittal2R1R2'); +}); + /** * Tests for {@link dwv.image.range3d}. * @@ -123,6 +473,103 @@ QUnit.test('Test 3 components iterator.', function (assert) { assert.equal(test3Max, i3Theo, '#3 3d iterator max'); }); +/** + * Tests for {@link dwv.image.getSliceIterator}. + * + * @function module:tests/image~getSliceIterator + */ +QUnit.test('Test getSliceIterator.', function (assert) { + + // test data + var testData0 = dwv.test.data.iterator0; + + var imgSize00 = new dwv.image.Size([ + testData0.ncols, testData0.nrows, 1 + ]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); + var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); + var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize00, imgSpacing0); + imgGeometry0.appendOrigin(new dwv.math.Point3D(0, 0, 1), 1); + imgGeometry0.appendOrigin(new dwv.math.Point3D(0, 0, 2), 2); + imgGeometry0.appendOrigin(new dwv.math.Point3D(0, 0, 3), 3); + var image0 = new dwv.image.Image(imgGeometry0, testData0.buffer); + + var isRescaled = false; + var viewOrientation; + + // axial + var getAxIter = function (orientation) { + return function (index) { + var position = new dwv.math.Index([0, 0, index]); + return dwv.image.getSliceIterator( + image0, position, isRescaled, orientation); + }; + }; + + // axial: xyz + viewOrientation = dwv.math.getIdentityMat33(); + dwv.test.checkIterator(assert, + getAxIter(viewOrientation), testData0.valuesAx, 'axial'); + // axial: yxz + /* eslint-disable array-element-newline */ + viewOrientation = new dwv.math.Matrix33([ + 0, 1, 0, + 1, 0, 0, + 0, 0, 1 + ]); + /* eslint-enable array-element-newline */ + dwv.test.checkIterator(assert, + getAxIter(viewOrientation), testData0.valuesAx2, 'axial2'); + + // coronal + var getCoroIter = function (orientation) { + return function (index) { + var position = new dwv.math.Index([0, index, 0]); + return dwv.image.getSliceIterator( + image0, position, isRescaled, orientation); + }; + }; + + // coronal: xzy + viewOrientation = dwv.math.getMatrixFromName('coronal'); + dwv.test.checkIterator(assert, + getCoroIter(viewOrientation), testData0.valuesCo, 'coronal'); + // coronal: zxy + /* eslint-disable array-element-newline */ + viewOrientation = new dwv.math.Matrix33([ + 0, 1, 0, + 0, 0, 1, + 1, 0, 0 + ]); + /* eslint-enable array-element-newline */ + dwv.test.checkIterator(assert, + getCoroIter(viewOrientation), testData0.valuesCo2, 'coronal2'); + + // sagittal + var getSagIter = function (orientation) { + return function (index) { + var position = new dwv.math.Index([index, 0, 0]); + return dwv.image.getSliceIterator( + image0, position, isRescaled, orientation); + }; + }; + + // sagittal: yzx + viewOrientation = dwv.math.getMatrixFromName('sagittal'); + dwv.test.checkIterator(assert, + getSagIter(viewOrientation), testData0.valuesSa, 'sagittal'); + // sagittal: zyx + /* eslint-disable array-element-newline */ + viewOrientation = new dwv.math.Matrix33([ + 0, 0, 1, + 0, 1, 0, + 1, 0, 0 + ]); + /* eslint-enable array-element-newline */ + dwv.test.checkIterator(assert, + getSagIter(viewOrientation), testData0.valuesSa2, 'sagittal2'); +}); + /** * Tests for {@link dwv.image.rangeRegion}. * diff --git a/tests/image/size.test.js b/tests/image/size.test.js new file mode 100644 index 0000000000..566c4bea21 --- /dev/null +++ b/tests/image/size.test.js @@ -0,0 +1,165 @@ +/** + * Tests for the 'image/size.js' file. + */ +// Do not warn if these variables were not defined before. +/* global QUnit */ + +/** + * Tests for {@link dwv.image.Size}. + * + * @function module:tests/image~size + */ +QUnit.test('Test Size.', function (assert) { + // error cases + assert.throws(function () { + new dwv.image.Size(); + }, + new Error('Cannot create size with no values.'), + 'size with undef values array.'); + assert.throws(function () { + new dwv.image.Size(null); + }, + new Error('Cannot create size with no values.'), + 'size with null values array.'); + assert.throws(function () { + new dwv.image.Size([]); + }, + new Error('Cannot create size with empty values.'), + 'size with empty values array.'); + assert.throws(function () { + new dwv.image.Size([2, 2, 0]); + }, + new Error('Cannot create size with non number or zero values.'), + 'size with zero values.'); + assert.throws(function () { + new dwv.image.Size([2, undefined, 2]); + }, + new Error('Cannot create size with non number or zero values.'), + 'size with undef values.'); + assert.throws(function () { + new dwv.image.Size([2, 'a', 2]); + }, + new Error('Cannot create size with non number or zero values.'), + 'size with string values.'); + + var size0 = new dwv.image.Size([2, 3, 4]); + // length + assert.equal(size0.length(), 3, 'length'); + // test its values + assert.equal(size0.get(0), 2, 'get 0'); + assert.equal(size0.get(1), 3, 'get 1'); + assert.equal(size0.get(2), 4, 'get 2'); + assert.equal(size0.get(3), undefined, 'get 3 (above dim)'); + // dim size + assert.equal(size0.getDimSize(0), 1, 'getDimSize 0'); + assert.equal(size0.getDimSize(1), 2, 'getDimSize 1'); + assert.equal(size0.getDimSize(2), 6, 'getDimSize 2'); + assert.equal(size0.getDimSize(3), 24, 'getDimSize 3'); + assert.equal(size0.getTotalSize(), 24, 'getTotalSize'); + assert.equal(size0.getDimSize(4), null, 'getDimSize 4 (above dim)'); + + // equality + assert.equal(size0.equals(null), false, 'equals null false'); + assert.equal(size0.equals(), false, 'equals undefined false'); + var size10 = new dwv.image.Size([2, 3]); + assert.equal(size0.equals(size10), false, 'equals different length false'); + + assert.equal(size0.equals(size0), true, 'equals self true'); + var size11 = new dwv.image.Size([2, 3, 4]); + assert.equal(size0.equals(size11), true, 'equals true'); + var size12 = new dwv.image.Size([3, 3, 4]); + assert.equal(size0.equals(size12), false, 'equals false'); + + // is in bounds + var index0 = new dwv.math.Index([0, 0, 0]); + assert.equal(size0.isInBounds(index0), true, 'isInBounds 0,0,0'); + index0 = new dwv.math.Index([0, 0]); + assert.equal(size0.isInBounds(index0), false, 'isInBounds 0,0'); + index0 = new dwv.math.Index([1, 2, 3]); + assert.equal(size0.isInBounds(index0), true, 'isInBounds max'); + index0 = new dwv.math.Index([2, 3, 4]); + assert.equal(size0.isInBounds(index0), false, 'isInBounds too big'); + index0 = new dwv.math.Index([-1, 2, 3]); + assert.equal(size0.isInBounds(index0), false, 'isInBounds too small'); + + // can scroll + var size20 = new dwv.image.Size([2, 1, 2]); + assert.equal(size20.moreThanOne(0), true, 'moreThanOne 20-0'); + assert.equal(size20.moreThanOne(1), false, 'moreThanOne 20-1'); + assert.equal(size20.moreThanOne(2), true, 'moreThanOne 20-2'); + assert.equal(size20.moreThanOne(3), false, 'moreThanOne 20-3'); + + // get 2D + assert.deepEqual(size0.get2D(), {x: 2, y: 3}, 'get2D 2,3,4'); + +}); + +/** + * Tests for {@link dwv.image.Size.indexToOffset}. + * + * @function module:tests/image~indexToOffset + */ +QUnit.test('Test index to and from offset.', function (assert) { + var size00 = new dwv.image.Size([4, 3, 2]); + var testData00 = [ + {values: [0, 0, 0], offset: 0}, + {values: [1, 0, 0], offset: 1}, + {values: [2, 0, 0], offset: 2}, + {values: [3, 0, 0], offset: 3}, + {values: [0, 1, 0], offset: 4}, + {values: [1, 1, 0], offset: 5}, + {values: [2, 1, 0], offset: 6}, + {values: [3, 1, 0], offset: 7}, + {values: [0, 2, 0], offset: 8}, + {values: [1, 2, 0], offset: 9}, + {values: [2, 2, 0], offset: 10}, + {values: [3, 2, 0], offset: 11}, + {values: [0, 0, 1], offset: 12}, + {values: [1, 0, 1], offset: 13}, + {values: [2, 0, 1], offset: 14}, + {values: [3, 0, 1], offset: 15}, + {values: [0, 1, 1], offset: 16}, + {values: [1, 1, 1], offset: 17}, + {values: [2, 1, 1], offset: 18}, + {values: [3, 1, 1], offset: 19}, + {values: [0, 2, 1], offset: 20}, + {values: [1, 2, 1], offset: 21}, + {values: [2, 2, 1], offset: 22}, + {values: [3, 2, 1], offset: 23} + ]; + for (var i = 0; i < testData00.length; ++i) { + var index = new dwv.math.Index(testData00[i].values); + var offset = testData00[i].offset; + assert.equal( + size00.indexToOffset(index), offset, 'indexToOffset #' + i); + assert.ok( + size00.offsetToIndex(offset).equals(index), 'offsetToIndex #' + i); + } + + // test indexToOffset with start + var size01 = new dwv.image.Size([5, 4, 3, 2]); + var index01 = new dwv.math.Index([0, 0, 0, 0]); + // error: start too big + assert.throws(function () { + size01.indexToOffset(index01, 4); + }, + new Error('Invalid start value for indexToOffset'), + 'indexToOffset start too big'); + // error: index bad length + var index02 = new dwv.math.Index([0, 0, 0]); + assert.throws(function () { + size01.indexToOffset(index02, 2); + }, + new Error('Incompatible index and size length'), + 'indexToOffset start index bad length'); + // no error + assert.equal(size01.indexToOffset(index01, 2), 0, 'indexToOffset start #0'); + var index03 = new dwv.math.Index([0, 0, 1, 0]); + assert.equal(size01.indexToOffset(index03, 2), 1, 'indexToOffset start #1'); + var index04 = new dwv.math.Index([0, 0, 0, 1]); + assert.equal(size01.indexToOffset(index04, 2), 3, 'indexToOffset start #2'); + var index05 = new dwv.math.Index([0, 0, 3, 2]); + assert.equal(size01.indexToOffset(index05, 2), 9, 'indexToOffset start #3'); + var index06 = new dwv.math.Index([0, 0, 3, 2]); + assert.equal(size01.indexToOffset(index06, 3), 2, 'indexToOffset start #4'); +}); diff --git a/tests/image/spacing.test.js b/tests/image/spacing.test.js new file mode 100644 index 0000000000..dd8590c732 --- /dev/null +++ b/tests/image/spacing.test.js @@ -0,0 +1,24 @@ +/** + * Tests for the 'image/spacing.js' file. + */ +// Do not warn if these variables were not defined before. +/* global QUnit */ + +/** + * Tests for {@link dwv.image.Spacing}. + * + * @function module:tests/image~spacing + */ +QUnit.test('Test Spacing.', function (assert) { + var spacing0 = new dwv.image.Spacing([2, 3, 4]); + // test its values + assert.equal(spacing0.get(0), 2, 'getColumnSpacing'); + assert.equal(spacing0.get(1), 3, 'getRowSpacing'); + assert.equal(spacing0.get(2), 4, 'getSliceSpacing'); + // equality + assert.equal(spacing0.equals(spacing0), 1, 'equals self true'); + var spacing1 = new dwv.image.Spacing([2, 3, 4]); + assert.equal(spacing0.equals(spacing1), 1, 'equals true'); + var spacing2 = new dwv.image.Spacing([3, 3, 4]); + assert.equal(spacing0.equals(spacing2), 0, 'equals false'); +}); diff --git a/tests/image/view.test.js b/tests/image/view.test.js index 0eb39be840..c5c3e03093 100644 --- a/tests/image/view.test.js +++ b/tests/image/view.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('view'); /** * Tests for {@link dwv.image.View} listeners. @@ -13,15 +12,15 @@ QUnit.module('view'); QUnit.test('Test listeners.', function (assert) { // create an image var size0 = 4; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); image0.setMeta({BitsStored: 8}); // create a view var view0 = new dwv.image.View(image0); @@ -34,14 +33,14 @@ QUnit.test('Test listeners.', function (assert) { assert.equal(event.ww, 1, 'Expected call to listener2.'); }; // with two listeners - view0.addEventListener('wlcenterchange', listener1); - view0.addEventListener('wlcenterchange', listener2); + view0.addEventListener('wlchange', listener1); + view0.addEventListener('wlchange', listener2); view0.setWindowLevel(0, 1); // without listener2 - view0.removeEventListener('wlcenterchange', listener2); + view0.removeEventListener('wlchange', listener2); view0.setWindowLevel(0, 2); // without listener1 - view0.removeEventListener('wlcenterchange', listener1); + view0.removeEventListener('wlchange', listener1); view0.setWindowLevel(1, 1); }); @@ -53,15 +52,15 @@ QUnit.test('Test listeners.', function (assert) { QUnit.test('Test playback milliseconds.', function (assert) { // create an image var size0 = 4; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); image0.setMeta({RecommendedDisplayFrameRate: 20}); // create a view @@ -93,15 +92,15 @@ QUnit.test('Test playback milliseconds.', function (assert) { QUnit.test('Test generate data MONO.', function (assert) { // create an image var size0 = 2; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); image0.setMeta({BitsStored: 8}); // create a view var view0 = new dwv.image.View(image0); @@ -151,8 +150,8 @@ QUnit.test('Test generate data MONO.', function (assert) { QUnit.test('Test generate data RGB.', function (assert) { // create an image var size0 = 2; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; @@ -166,7 +165,7 @@ QUnit.test('Test generate data RGB.', function (assert) { buffer0[index + 2] = value; index += 3; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); image0.setPhotometricInterpretation('RGB'); image0.setMeta({BitsStored: 8}); // create a view @@ -219,7 +218,7 @@ QUnit.test('Test generate data RGB.', function (assert) { buffer1[index + 3] = 255; index += 4; } - var image1 = new dwv.image.Image(imgGeometry0, [buffer1]); + var image1 = new dwv.image.Image(imgGeometry0, buffer1); image1.setPhotometricInterpretation('RGB'); image1.setPlanarConfiguration(1); image1.setMeta({BitsStored: 8}); @@ -250,15 +249,15 @@ QUnit.test('Test generate data RGB.', function (assert) { QUnit.test('Test generate data timing.', function (assert) { // create an image var size0 = 128; - var imgSize0 = new dwv.image.Size(size0, size0, 1); - var imgSpacing0 = new dwv.image.Spacing(1, 1, 1); + var imgSize0 = new dwv.image.Size([size0, size0, 1]); + var imgSpacing0 = new dwv.image.Spacing([1, 1, 1]); var imgOrigin0 = new dwv.math.Point3D(0, 0, 0); var imgGeometry0 = new dwv.image.Geometry(imgOrigin0, imgSize0, imgSpacing0); var buffer0 = []; for (var i = 0; i < size0 * size0; ++i) { buffer0[i] = i; } - var image0 = new dwv.image.Image(imgGeometry0, [buffer0]); + var image0 = new dwv.image.Image(imgGeometry0, buffer0); image0.setMeta({BitsStored: 8}); // create a view var view0 = new dwv.image.View(image0); diff --git a/tests/math/bucketQueue.test.js b/tests/math/bucketQueue.test.js new file mode 100644 index 0000000000..2c8cbb3663 --- /dev/null +++ b/tests/math/bucketQueue.test.js @@ -0,0 +1,47 @@ +/** + * Tests for the 'math/BucketQueue.js' file. + */ +/** @module tests/math */ +// Do not warn if these variables were not defined before. +/* global QUnit */ +QUnit.module('math'); + +/** + * Tests for {@link dwv.math.BucketQueue}. + * + * @function module:tests/math~BucketQueue + */ +QUnit.test('Test BucketQueue.', function (assert) { + var queue00 = new dwv.math.BucketQueue(); + // isEmpty + assert.equal(queue00.isEmpty(), true, 'create isEmpty'); + + var itemEquals = function (rhs) { + return rhs && rhs.a === this.a && rhs.b === this.b; + }; + var item00 = { + a: 0, + b: 0, + equals: itemEquals + }; + + // push + queue00.push(item00); + assert.equal(queue00.isEmpty(), false, 'push isEmpty'); + + // pop + var resItem02 = queue00.pop(); + assert.equal(queue00.isEmpty(), true, 'pop isEmpty'); + assert.ok(resItem02.equals(item00), 'pop item'); + + // remove + var item01 = { + a: 0, + b: 1, + equals: itemEquals + }; + queue00.push(item00); + queue00.push(item01); + assert.equal(queue00.remove(item00), true, 'remove'); + assert.equal(queue00.isEmpty(), false, 'remove isEmpty'); +}); diff --git a/tests/math/circle.test.js b/tests/math/circle.test.js index 20263d52e0..4d7108c85f 100644 --- a/tests/math/circle.test.js +++ b/tests/math/circle.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-circle'); /** * Tests for {@link dwv.math.Circle}. @@ -52,7 +51,7 @@ QUnit.test('Test Circle quantify.', function (assert) { return [1, 1]; }, getCurrentPosition: function () { - return {k: 0}; + return new dwv.math.Index([0, 0, 0]); }, getImageVariableRegionValues: function () { return [0, 1, 1, 0, 0, 1, 1, 0]; diff --git a/tests/math/ellipse.test.js b/tests/math/ellipse.test.js index fe5da81fbd..beac6eb5b3 100644 --- a/tests/math/ellipse.test.js +++ b/tests/math/ellipse.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-ellipse'); /** * Tests for {@link dwv.math.Ellipse}. @@ -57,7 +56,7 @@ QUnit.test('Test Ellipse quantify.', function (assert) { return [1, 1]; }, getCurrentPosition: function () { - return {k: 0}; + return new dwv.math.Index([0, 0, 0]); }, getImageVariableRegionValues: function () { return [0, 1, 1, 0, 0, 1, 1, 0]; diff --git a/tests/math/index.test.js b/tests/math/index.test.js new file mode 100644 index 0000000000..3deb892977 --- /dev/null +++ b/tests/math/index.test.js @@ -0,0 +1,109 @@ +/** + * Tests for the 'math/index.js' file. + */ +// Do not warn if these variables were not defined before. +/* global QUnit */ + +/** + * Tests for {@link dwv.math.Index}. + * + * @function module:tests/math~Index + */ +QUnit.test('Test Index.', function (assert) { + // error cases + assert.throws(function () { + new dwv.math.Index(); + }, + new Error('Cannot create index with no values.'), + 'index with undef values array.'); + assert.throws(function () { + new dwv.math.Index(null); + }, + new Error('Cannot create index with no values.'), + 'index with null values array.'); + assert.throws(function () { + new dwv.math.Index([]); + }, + new Error('Cannot create index with empty values.'), + 'index with empty values array.'); + assert.throws(function () { + new dwv.math.Index([2, undefined, 2]); + }, + new Error('Cannot create index with non number values.'), + 'index with undef values.'); + assert.throws(function () { + new dwv.math.Index([2, 'a', 2]); + }, + new Error('Cannot create index with non number values.'), + 'index with string values.'); + + var i0 = new dwv.math.Index([1, 2, 3]); + // getX + assert.equal(i0.get(0), 1, 'get0'); + // getY + assert.equal(i0.get(1), 2, 'get1'); + // getZ + assert.equal(i0.get(2), 3, 'get2'); + + // equals: true + var i10 = new dwv.math.Index([1, 2, 3]); + assert.equal(i10.equals(i10), true, 'equals true'); + // equals: false + assert.equal(i10.equals(null), false, 'null equals false'); + var i11 = new dwv.math.Index([1, 2]); + assert.equal(i10.equals(i11), false, 'length equals false'); + var i12 = new dwv.math.Index([3, 2, 1]); + assert.equal(i10.equals(i12), false, 'values equals false'); + + // to string + var i20 = new dwv.math.Index([1, 2, 3]); + assert.equal(i20.toString(), '(1,2,3)', 'toString'); + + // warning: values are NOT cloned. So this can happen: + var val3 = [1, 2, 3]; + var i30 = new dwv.math.Index(val3); + assert.equal(i30.get(0), 1, '[clone] get0'); + val3[0] = 4; + assert.equal(i30.get(0), 4, '[clone] get0'); + + // addition + var i40 = new dwv.math.Index([1, 2, 3]); + var i41 = new dwv.math.Index([2, 3, 4]); + var i42 = i40.add(i41); + assert.equal(i42.get(0), 3, '[add] get0'); + assert.equal(i42.get(1), 5, '[add] get1'); + assert.equal(i42.get(2), 7, '[add] get2'); +}); + +/** + * Tests for {@link dwv.math.Index} to and from stringId conversion. + * + * @function module:tests/math~Index + */ +QUnit.test('Test Point stringId.', function (assert) { + var i00 = new dwv.math.Index([1, 2, 3]); + var i00strId = '#0-1_#1-2_#2-3'; + assert.equal(i00.toStringId(), i00strId, 'toStringId #00'); + assert.ok(dwv.math.getIndexFromStringId(i00strId).equals(i00), + 'getFromStringId #00'); + + var i01 = new dwv.math.Index([0, 2, 3]); + var i01strId = '#1-2_#2-3'; + assert.equal(i01.toStringId([1, 2]), i01strId, 'toStringId #01'); + assert.ok(dwv.math.getIndexFromStringId(i01strId).equals(i01), + 'getFromStringId #01'); + + var i02 = new dwv.math.Index([0, 0, 3]); + var i02strId = '#2-3'; + assert.equal(i02.toStringId([2]), i02strId, 'toStringId #02'); + assert.ok(dwv.math.getIndexFromStringId(i02strId).equals(i02), + 'getFromStringId #02'); + + // error case + var i10 = new dwv.math.Index([0, 0, 0]); + assert.throws(function () { + i10.toStringId([3]); + }, + new Error('Non valid dimension for toStringId.'), + 'toStringId error'); +}); diff --git a/tests/math/line.test.js b/tests/math/line.test.js index 3358ec57d9..ac5db3d6cf 100644 --- a/tests/math/line.test.js +++ b/tests/math/line.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-line'); /** * Tests for {@link dwv.math.Line}. diff --git a/tests/math/matrix.test.js b/tests/math/matrix.test.js index c881db7959..7f2a68121d 100644 --- a/tests/math/matrix.test.js +++ b/tests/math/matrix.test.js @@ -4,28 +4,32 @@ /** @module tests/math */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('matrix'); /** * Tests for {@link dwv.math.Matrix33}. * * @function module:tests/math~Matrix33 */ -QUnit.test('Test Matrix44.', function (assert) { +QUnit.test('Test Matrix33.', function (assert) { var m0 = new dwv.math.getIdentityMat33(); var m1 = new dwv.math.getIdentityMat33(); - var m2 = new dwv.math.Matrix33( + /* eslint-disable array-element-newline */ + var m2 = new dwv.math.Matrix33([ 1, 2, 3, 4, 5, 6, - 7, 8, 9); - var m3 = new dwv.math.Matrix33( + 7, 8, 9 + ]); + var m3 = new dwv.math.Matrix33([ 1.001, 2.001, 3.001, 4.001, 5.001, 6.001, - 7.001, 8.001, 9.001); - var m4 = new dwv.math.Matrix33( + 7.001, 8.001, 9.001 + ]); + var m4 = new dwv.math.Matrix33([ 1.002, 2.002, 3.002, 4.002, 5.002, 6.002, - 7.002, 8.002, 9.002); + 7.002, 8.002, 9.002 + ]); + /* eslint-enable array-element-newline */ // equals assert.equal(m0.equals(m1), true, 'equals true'); @@ -43,3 +47,194 @@ QUnit.test('Test Matrix44.', function (assert) { assert.equal(m3.equals(m4, 0.01), true, 'equals true'); assert.equal(m3.equals(m4, 0.001), false, 'equals false'); }); + +/** + * Tests for {@link dwv.math.Matrix33} vector multiplication. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 multiply vector.', function (assert) { + // id + var id = dwv.math.getIdentityMat33(); + var arr00 = [1, 2, 3]; + var resArr00 = [1, 2, 3]; + assert.deepEqual(id.multiplyArray3D(arr00), resArr00, 'id multiply array'); + var v00 = new dwv.math.Vector3D(1, 2, 3); + var resV00 = new dwv.math.Vector3D(1, 2, 3); + assert.ok(id.multiplyVector3D(v00).equals(resV00), 'id multiply vector'); + var i00 = new dwv.math.Index(arr00); + var resI00 = new dwv.math.Index(resArr00); + assert.ok(id.multiplyIndex3D(i00).equals(resI00), 'id multiply index'); + + // zero + var m00 = new dwv.math.Matrix33([ + 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + var resArr01 = [0, 0, 0]; + assert.deepEqual(m00.multiplyArray3D(arr00), resArr01, 'zero multiply array'); + var v01 = new dwv.math.Vector3D(1, 2, 3); + var resV01 = new dwv.math.Vector3D(0, 0, 0); + assert.ok(m00.multiplyVector3D(v01).equals(resV01), 'zero multiply vector'); + var i01 = new dwv.math.Index(arr00); + var resI01 = new dwv.math.Index(resArr01); + assert.ok(m00.multiplyIndex3D(i01).equals(resI01), 'zero multiply index'); + + // test #10 + var m10 = new dwv.math.Matrix33([ + 1, 2, 3, 4, 5, 6, 7, 8, 9 + ]); + var resArr10 = [14, 32, 50]; + assert.deepEqual(m10.multiplyArray3D(arr00), resArr10, 'multiply array #10'); + var v10 = new dwv.math.Vector3D(1, 2, 3); + var resV10 = new dwv.math.Vector3D(14, 32, 50); + assert.ok(m10.multiplyVector3D(v10).equals(resV10), 'multiply vector #10'); + var i10 = new dwv.math.Index(arr00); + var resI10 = new dwv.math.Index(resArr10); + assert.ok(m10.multiplyIndex3D(i10).equals(resI10), 'multiply index #10'); +}); + +/** + * Tests for {@link dwv.math.Matrix33} multiplication. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 multiply.', function (assert) { + // id + var id = dwv.math.getIdentityMat33(); + var idid = id.multiply(id); + assert.ok(idid.equals(id), 'multiply id'); + + // zero + var m00 = new dwv.math.Matrix33([ + 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + var m00id = m00.multiply(id); + assert.ok(m00id.equals(m00), 'multiply #0'); + + // test #10 + var m10 = new dwv.math.Matrix33([ + 1, 2, 3, 4, 5, 6, 7, 8, 9 + ]); + var m11 = new dwv.math.Matrix33([ + 2, 3, 4, 5, 6, 7, 8, 9, 10 + ]); + var m10m11 = m10.multiply(m11); + var res10 = new dwv.math.Matrix33([ + 36, 42, 48, 81, 96, 111, 126, 150, 174 + ]); + assert.ok(m10m11.equals(res10), 'multiply #1'); +}); + +/** + * Tests for {@link dwv.math.Matrix33} inversion. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 inverse.', function (assert) { + // id + var id = dwv.math.getIdentityMat33(); + var invid = id.getInverse(); + assert.ok(invid.equals(id), 'inverse id'); + + // double inverse + var invinvid = invid.getInverse(); + assert.ok(invinvid.equals(id), 'inverse id twice'); + + // test #10 + var m10 = new dwv.math.Matrix33([ + 1, 4, 5, 7, 2, 6, 8, 9, 3 + ]); + var invm10 = m10.getInverse(); + var res10 = new dwv.math.Matrix33([ + -48 / 295, + 33 / 295, + 14 / 295, + 27 / 295, + -37 / 295, + 29 / 295, + 47 / 295, + 23 / 295, + -26 / 295 + ]); + assert.ok(invm10.equals(res10), 'inverse #1'); + + // double inverse + var invinvm10 = invm10.getInverse(); + assert.ok(invinvm10.equals(m10, dwv.math.BIG_EPSILON), 'inverse #1 twice'); +}); + +/** + * Tests for {@link dwv.math.Matrix33} getAbs. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 abs.', function (assert) { + var m00 = new dwv.math.Matrix33([ + -1, -2, -3, -4, -5, -6, -7, -8, -9 + ]); + var theo00 = new dwv.math.Matrix33([ + 1, 2, 3, 4, 5, 6, 7, 8, 9 + ]); + assert.ok(m00.getAbs().equals(theo00), 'Matrix33 abs'); +}); + +/** + * Tests for {@link dwv.math.Matrix33} asOneAndZeros. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 asOneAndZeros.', function (assert) { + // test #00 + var m00 = new dwv.math.Matrix33([ + 1, 2, 3, 4, 5, 6, 7, 8, 9 + ]); + var theo00 = new dwv.math.Matrix33([ + 0, 0, 1, 0, 0, 1, 0, 0, 1 + ]); + assert.ok(m00.asOneAndZeros().equals(theo00), 'Matrix33 asOneAndZeros #00'); + + // test #01 + var m01 = new dwv.math.Matrix33([ + 3, 2, 1, 100, 99, 98, 5.5, 5.6, 5.4 + ]); + var theo01 = new dwv.math.Matrix33([ + 1, 0, 0, 1, 0, 0, 0, 1, 0 + ]); + assert.ok(m01.asOneAndZeros().equals(theo01), 'Matrix33 asOneAndZeros #01'); +}); + +/** + * Tests for {@link dwv.math.Matrix33} factories. + * + * @function module:tests/math~Matrix33 + */ +QUnit.test('Test Matrix33 factories.', function (assert) { + // test #00 + var m00 = dwv.math.getIdentityMat33(); + var theo00 = new dwv.math.Matrix33([ + 1, 0, 0, 0, 1, 0, 0, 0, 1 + ]); + assert.ok(m00.equals(theo00), 'Matrix33 factory id'); + + // test #01 + var m01 = dwv.math.getMatrixFromName('axial'); + assert.ok(m01.equals(theo00), 'Matrix33 factory axial'); + + // test #02 + var m02 = dwv.math.getMatrixFromName('coronal'); + var theo02 = new dwv.math.Matrix33([ + 1, 0, 0, 0, 0, 1, 0, 1, 0 + ]); + assert.ok(m02.equals(theo02), 'Matrix33 factory coronal'); + + // test #03 + var m03 = dwv.math.getMatrixFromName('sagittal'); + var theo03 = new dwv.math.Matrix33([ + 0, 0, 1, 1, 0, 0, 0, 1, 0 + ]); + assert.ok(m03.equals(theo03), 'Matrix33 factory sagittal'); + + // test #04 + var m04 = dwv.math.getMatrixFromName('godo'); + assert.equal(m04, null, 'Matrix33 factory unknown name'); +}); diff --git a/tests/math/path.test.js b/tests/math/path.test.js index 6253db4c88..94c8aedbc2 100644 --- a/tests/math/path.test.js +++ b/tests/math/path.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-path'); /** * Tests for {@link dwv.math.Path}. @@ -26,8 +25,30 @@ QUnit.test('Test Path.', function (assert) { assert.equal(path0.getPoint(0), p0, 'getPoint first'); // getPoint second assert.equal(path0.getPoint(1), p1, 'getPoint second'); + var p2 = new dwv.math.Point2D(1, 1); + var p3 = new dwv.math.Point2D(1, 2); + path0.addPoints([p2, p3]); + assert.equal(path0.getPoint(0), p0, 'addPoints getPoint first'); + assert.equal(path0.getPoint(1), p1, 'addPoints getPoint second'); + assert.equal(path0.getPoint(2), p2, 'addPoints getPoint third'); + assert.equal(path0.getPoint(3), p3, 'addPoints getPoint fourth'); + // add first point a control point path0.addControlPoint(p0); // check if control point assert.equal(path0.isControlPoint(p0), 1, 'isControlPoint'); + // bad control point + var p10 = new dwv.math.Point2D(1, 1); + assert.throws(function () { + path0.addControlPoint(p10); + }, + new Error('Cannot mark a non registered point as control point.'), + 'bad control point'); + + // append path + var path1 = new dwv.math.Path(); + var p4 = new dwv.math.Point2D(1, 3); + path1.addPoint(p4); + path0.appenPath(path1); + assert.equal(path0.getPoint(4), p4, 'appenPath getPoint fifth'); }); diff --git a/tests/math/point.test.js b/tests/math/point.test.js index a3d947c6dd..54bd4b8665 100644 --- a/tests/math/point.test.js +++ b/tests/math/point.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('point'); /** * Tests for {@link dwv.math.Point2D}. @@ -44,34 +43,6 @@ QUnit.test('Test Point2D.', function (assert) { }); -/** - * Tests for {@link dwv.math.FastPoint2D}. - * - * @function module:tests/math~FastPoint2D - */ -QUnit.test('Test FastPoint2D.', function (assert) { - var p0 = new dwv.math.FastPoint2D(1, 2); - // x - assert.equal(p0.x, 1, 'x'); - // y - assert.equal(p0.y, 2, 'y'); - // can modify x - p0.x = 3; - assert.equal(p0.x, 3, 'modified x'); - // can modify y - p0.y = 4; - assert.equal(p0.y, 4, 'modified y'); - // equals: true - var p1 = new dwv.math.FastPoint2D(3, 4); - assert.equal(p0.equals(p1), true, 'equals true'); - // equals: false - assert.equal(p0.equals(null), false, 'null equals false'); - var p2 = new dwv.math.FastPoint2D(4, 3); - assert.equal(p0.equals(p2), false, 'equals false'); - // to string - assert.equal(p0.toString(), '(3, 4)', 'toString'); -}); - /** * Tests for {@link dwv.math.Point3D}. * @@ -116,34 +87,57 @@ QUnit.test('Test Point3D.', function (assert) { }); /** - * Tests for {@link dwv.math.Index3D}. + * Tests for {@link dwv.math.Point}. * - * @function module:tests/math~Index3D + * @function module:tests/math~Point */ -QUnit.test('Test Index3D.', function (assert) { - var i0 = new dwv.math.Index3D(1, 2, 3); +QUnit.test('Test Point.', function (assert) { + var p0 = new dwv.math.Point([1, 2, 3]); // getX - assert.equal(i0.getI(), 1, 'getI'); + assert.equal(p0.get(0), 1, 'getX'); // getY - assert.equal(i0.getJ(), 2, 'getJ'); + assert.equal(p0.get(1), 2, 'getY'); // getZ - assert.equal(i0.getK(), 3, 'getK'); - // can't modify internal i - i0.i = 3; - assert.equal(i0.getI(), 1, 'getI after .i'); - // can't modify internal j - i0.j = 3; - assert.equal(i0.getJ(), 2, 'getJ after .j'); - // can't modify internal k - i0.k = 3; - assert.equal(i0.getK(), 3, 'getK after .k'); + assert.equal(p0.get(2), 3, 'getZ'); // equals: true - var i1 = new dwv.math.Index3D(1, 2, 3); - assert.equal(i0.equals(i1), true, 'equals true'); + var p1 = new dwv.math.Point([1, 2, 3]); + assert.equal(p0.equals(p1), true, 'equals true'); // equals: false - assert.equal(i0.equals(null), false, 'null equals false'); - var i2 = new dwv.math.Index3D(3, 2, 1); - assert.equal(i0.equals(i2), false, 'equals false'); + assert.equal(p0.equals(null), false, 'null equals false'); + var p2 = new dwv.math.Point([3, 2, 1]); + assert.equal(p0.equals(p2), false, 'equals false'); // to string - assert.equal(i0.toString(), '(1, 2, 3)', 'toString'); + assert.equal(p0.toString(), '(1,2,3)', 'toString'); + + // compare + var res30 = p0.compare(p0); + assert.equal(res30.length, 0, '[compare] #0'); + var p31 = new dwv.math.Point([2, 3, 4]); + var res31 = p0.compare(p31); + assert.equal(res31.length, 3, '[compare] #1 length'); + assert.equal(res31[0], 0, '[compare] #1 [0]'); + assert.equal(res31[1], 1, '[compare] #1 [1]'); + assert.equal(res31[2], 2, '[compare] #1 [2]'); + var p32 = new dwv.math.Point([1, 3, 4]); + var res32 = p0.compare(p32); + assert.equal(res32.length, 2, '[compare] #2 length'); + assert.equal(res32[0], 1, '[compare] #2 [0]'); + assert.equal(res32[1], 2, '[compare] #2 [1]'); + + // addition + var p40 = new dwv.math.Point([2, 3, 4]); + var res40 = p0.add(p40); + assert.equal(res40.get(0), 3, '[add] get0'); + assert.equal(res40.get(1), 5, '[add] get1'); + assert.equal(res40.get(2), 7, '[add] get2'); + + // mergeWith3D + var p50 = new dwv.math.Point([1, 2, 3, 4]); + var p3D0 = new dwv.math.Point3D(5, 6, 7); + var res50 = p50.mergeWith3D(p3D0); + assert.equal(res50.length(), 4, '[merge] #0 length'); + assert.equal(res50.get(0), 5, '[merge] #0 [0]'); + assert.equal(res50.get(1), 6, '[merge] #0 [1]'); + assert.equal(res50.get(2), 7, '[merge] #0 [2]'); + assert.equal(res50.get(3), 4, '[merge] #0 [3]'); }); diff --git a/tests/math/rectangle.test.js b/tests/math/rectangle.test.js index 14257bb43a..fea76115c3 100644 --- a/tests/math/rectangle.test.js +++ b/tests/math/rectangle.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-rectangle'); /** * Tests for {@link dwv.math.Rectangle}. @@ -62,7 +61,7 @@ QUnit.test('Test Rectangle quantify.', function (assert) { return [1, 1]; }, getCurrentPosition: function () { - return {k: 0}; + return new dwv.math.Index([0, 0, 0]); }, getImageRegionValues: function () { return [0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; diff --git a/tests/math/roi.test.js b/tests/math/roi.test.js index d692040b1a..40bb4732c5 100644 --- a/tests/math/roi.test.js +++ b/tests/math/roi.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('shape-roi'); /** * Tests for {@link dwv.math.ROI}. diff --git a/tests/math/stats.test.js b/tests/math/stats.test.js index 7d17501743..2fdf5e5bae 100644 --- a/tests/math/stats.test.js +++ b/tests/math/stats.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('stats'); /** * Tests for {@link dwv.math.Stats.equals}. diff --git a/tests/math/vector.test.js b/tests/math/vector.test.js index 1298b9d81a..e77dc66805 100644 --- a/tests/math/vector.test.js +++ b/tests/math/vector.test.js @@ -3,7 +3,6 @@ */ // Do not warn if these variables were not defined before. /* global QUnit */ -QUnit.module('vector'); /** * Tests for {@link dwv.math.Vector3D}. diff --git a/tests/pacs/viewer.html b/tests/pacs/viewer.html index 7549987bd8..aa06f0dd5a 100644 --- a/tests/pacs/viewer.html +++ b/tests/pacs/viewer.html @@ -2,7 +2,7 @@ -DWV DICOM Check +DWV Test Viewer @@ -11,15 +11,44 @@ body { font-family: Arial, Helvetica, sans-serif; } +table, td, th { + border: 1px solid #aaa; +} +table { + border-collapse: collapse; +} +td, th { + padding: 10px; +} +progress { + width: 40%; +} +span { + font-size: small; + font-style: italic; +} .input { margin-bottom: 10px; padding: 5px; background-color: gainsboro; } #fileinput { - width: 80%; + width: 50%; border: 1px dotted gray; } +.layerGroup { + display:inline-block; + height: 350px; + width: 350px; + margin: 5px; + background-color: gainsboro; +} +.layer { + position: absolute; +} +.line { + padding: 5px; +} @@ -27,15 +56,18 @@ - + + + + @@ -46,8 +78,11 @@ + + + @@ -61,14 +96,17 @@ + + + @@ -79,7 +117,17 @@ - + + + + + + + + + + + @@ -38,14 +14,17 @@ - + + + + @@ -55,8 +34,11 @@ + + + @@ -68,6 +50,7 @@ + @@ -91,35 +74,35 @@ // launch when page is loaded document.addEventListener("DOMContentLoaded", function (/*event*/) { - dwv.addDataLine("dwv0", "leadtools-8BitsJpegLossyGrayScale", { + dwv.addDataLine(0, "leadtools-8BitsJpegLossyGrayScale", { 'origin': 'LeadTools', 'path': '8BitsJpegLossyGrayScale.zip', 't-syntax': '1.2.840.10008.1.2.4.50 (jpeg baseline)', 'modality': 'OT', 'photo': 'MONOCHROME2', 'bits': '8-8-7', 'pixel-vr': 'OW', }); - dwv.addDataLine("dwv1", "nema-mr1_jply", { + dwv.addDataLine(1, "nema-mr1_jply", { 'origin': 'Nema WG04', 'path': 'compsamples_jpeg/IMAGES/JPLY/MR1_JPLY', 't-syntax': '1.2.840.10008.1.2.4.51 (jpeg baseline)', 'modality': 'MR', 'photo': 'MONOCHROME2', 'bits': '16-12-11', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv2", "nema-nm1_jply", { + dwv.addDataLine(2, "nema-nm1_jply", { 'origin': 'Nema WG04', 'path': 'compsamples_jpeg/IMAGES/JPLY/NM1_JPLY', 't-syntax': '1.2.840.10008.1.2.4.51 (jpeg baseline)', 'modality': 'NM', 'photo': 'MONOCHROME2', 'bits': '16-12-11', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv3", "nema-ct1_jpll", { + dwv.addDataLine(3, "nema-ct1_jpll", { 'origin': 'Nema WG04', 'path': 'compsamples_jpeg/IMAGES/JPLL/CT1_JPLL', 't-syntax': '1.2.840.10008.1.2.4.70 (jpeg lossless)', 'modality': 'CT', 'photo': 'MONOCHROME2', 'bits': '16-16-15', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv4", "nema-nm1_jpll", { + dwv.addDataLine(4, "nema-nm1_jpll", { 'origin': 'Nema WG04', 'path': 'compsamples_jpeg/IMAGES/JPLL/NM1_JPLL', 't-syntax': '1.2.840.10008.1.2.4.70 (jpeg lossless)', diff --git a/tests/visual/index-jpeg2000.html b/tests/visual/index-jpeg2000.html index 0e70145082..f3f27f598c 100644 --- a/tests/visual/index-jpeg2000.html +++ b/tests/visual/index-jpeg2000.html @@ -6,31 +6,7 @@ - - + @@ -39,14 +15,17 @@ - + + + + @@ -56,8 +35,11 @@ + + + @@ -69,6 +51,7 @@ + @@ -92,35 +75,35 @@ // launch when page is loaded document.addEventListener("DOMContentLoaded", function (/*event*/) { - dwv.addDataLine("dwv0", "osirix-cerebrix", { + dwv.addDataLine(0, "osirix-cerebrix", { 'origin': 'Osirix', 'path': 'CEREBRIX/Neuro Crane/Axial_T1 - 5352/IM-0001-0100.dcm', 't-syntax': '1.2.840.10008.1.2.4.91 (jpeg2000)', 'modality': 'SC', 'photo': 'MONOCHROME2', 'bits': '16-16-15', 'pixel-vr': 'OW', }); - dwv.addDataLine("dwv1", "nema-ct1_j2ki", { + dwv.addDataLine(1, "nema-ct1_j2ki", { 'origin': 'Nema WG04', 'path': 'compsamples_j2k/IMAGES/J2KI/CT1_J2KI', 't-syntax': '1.2.840.10008.1.2.4.91 (jpeg2000)', 'modality': 'CT', 'photo': 'MONOCHROME2', 'bits': '16-16-15', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv2", "nema-us1_j2ki", { + dwv.addDataLine(2, "nema-us1_j2ki", { 'origin': 'Nema WG04', 'path': 'compsamples_j2k/IMAGES/J2KI/US1_J2KI', 't-syntax': '1.2.840.10008.1.2.4.91 (jpeg2000)', 'modality': 'US', 'photo': 'YBR_ICT (planar=0)', 'bits': '8-8-7', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv3", "nema-nm1_j2kr", { + dwv.addDataLine(3, "nema-nm1_j2kr", { 'origin': 'Nema WG04', 'path': 'compsamples_j2k/IMAGES/J2KR/NM1_J2KR', 't-syntax': '1.2.840.10008.1.2.4.90 (jpeg2000)', 'modality': 'NM', 'photo': 'MONOCHROME2', 'bits': '16-16-15', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv4", "nema-vl2_j2kr", { + dwv.addDataLine(4, "nema-vl2_j2kr", { 'origin': 'Nema WG04', 'path': 'compsamples_j2k/IMAGES/J2KR/VL2_J2KR', 't-syntax': '1.2.840.10008.1.2.4.90 (jpeg2000)', diff --git a/tests/visual/index-rle.html b/tests/visual/index-rle.html index a803769d27..048d519d0a 100644 --- a/tests/visual/index-rle.html +++ b/tests/visual/index-rle.html @@ -2,49 +2,28 @@ -DWV DICOM jpg Check +DWV DICOM rle Check - - + - + + + + @@ -54,8 +33,11 @@ + + + @@ -67,6 +49,7 @@ + @@ -90,14 +73,14 @@ // launch when page is loaded document.addEventListener("DOMContentLoaded", function (/*event*/) { - dwv.addDataLine("dwv0", "leadtools-flowers-8-mono2-rle", { + dwv.addDataLine(0, "leadtools-flowers-8-mono2-rle", { 'origin': 'leadtools', 'path': 'leadtools-flowers-8-mono2-rle.dcm', 't-syntax': '1.2.840.10008.1.2.5 (rle)', 'modality': 'OT', 'photo': 'MONOCHROME2', 'bits': '8-8-7', 'pixel-vr': 'OW', }); - dwv.addDataLine("dwv1", "leadtools-flowers-16-mono2-rle", { + dwv.addDataLine(1, "leadtools-flowers-16-mono2-rle", { 'origin': 'leadtools', 'path': 'leadtools-flowers-16-mono2-rle.dcm', 't-syntax': '1.2.840.10008.1.2.5 (rle)', diff --git a/tests/visual/index.html b/tests/visual/index.html index 0519ec1bea..f1a7bd186b 100644 --- a/tests/visual/index.html +++ b/tests/visual/index.html @@ -6,43 +6,22 @@ - - + - + + + + @@ -52,8 +31,11 @@ + + + @@ -65,6 +47,7 @@ + @@ -88,49 +71,49 @@ // launch when page is loaded document.addEventListener("DOMContentLoaded", function (/*event*/) { - dwv.addDataLine("dwv0", "osirix-toutatix-100", { + dwv.addDataLine(0, "osirix-toutatix-100", { 'origin': 'Osirix', 'path': 'TOUTATIX/Cardiac 1CTA_CORONARY_ARTERIES_TESTBOLUS (Adult)/Heart w-o 1.5 B25f 55% /IM-0001-0100.dcm', 't-syntax': '1.2.840.10008.1.2.1', 'modality': 'CT', 'photo': 'Monochrome2', 'bits': '16-12-11', 'pixel-vr': 'OW', }); - dwv.addDataLine("dwv1", "osirix-goudurix", { + dwv.addDataLine(1, "osirix-goudurix", { 'origin': 'Osirix', 'path': 'GOUDURIX/Specials 1_CORONARY_CTA_COMBI_SMH/70 % 1.0 B30f/IM-0001-0100.dcm', 't-syntax': '1.2.840.10008.1.2', 'modality': 'CT', 'photo': 'Monochrome2', 'bits': '16-12-11', 'pixel-vr': 'OX', }); - dwv.addDataLine("dwv2", "dicompyler-ct.0", { + dwv.addDataLine(2, "dicompyler-ct.0", { 'origin': 'dicompyler', 'path': 'dicompyler/ct/ct.0.dcm', 't-syntax': '1.2.840.10008.1.2', 'modality': 'CT', 'photo': 'Monochrome2', 'bits': '16-16-15', 'pixel-vr': 'OX', }); - dwv.addDataLine("dwv3", "gdcm-CR-MONO1-10-chest", { + dwv.addDataLine(3, "gdcm-CR-MONO1-10-chest", { 'origin': 'GDCM (+ DCIM prefix)', 'path': 'CR-MONO1-10-chest.dcm', 't-syntax': '1.2.840.10008.1.2.1', 'modality': 'CR', 'photo': 'Monochrome1', 'bits': '16-10-9', 'pixel-vr': 'OW', }); - dwv.addDataLine("dwv4", "gdcm-CT-MONO2-8-abdo", { + dwv.addDataLine(4, "gdcm-CT-MONO2-8-abdo", { 'origin': 'GDCM', 'path': 'CT-MONO2-8-abdo.dcm', 't-syntax': '1.2.840.10008.1.2', 'modality': 'CT', 'photo': 'Monochrome2', 'bits': '8-8-7', 'pixel-vr': 'OX', }); - dwv.addDataLine("dwv5", "gdcm-US-RGB-8-epicard", { + dwv.addDataLine(5, "gdcm-US-RGB-8-epicard", { 'origin': 'GDCM', 'path': 'US-RGB-8-epicard.dcm', 't-syntax': '1.2.840.10008.1.2.2', 'modality': 'US', 'photo': 'RGB (planar=1)', 'bits': '8-8-7', 'pixel-vr': 'OB', }); - dwv.addDataLine("dwv6", "gdcm-US-RGB-8-esopecho", { + dwv.addDataLine(6, "gdcm-US-RGB-8-esopecho", { 'origin': 'GDCM', 'path': 'US-RGB-8-esopecho.dcm', 't-syntax': '1.2.840.10008.1.2.1', diff --git a/tests/visual/style.css b/tests/visual/style.css new file mode 100644 index 0000000000..589a41b5bd --- /dev/null +++ b/tests/visual/style.css @@ -0,0 +1,37 @@ +body { + font-family: Arial, Helvetica, sans-serif; +} + +.snapshot { + float: left; + margin: 5px; + max-width: 200px; + max-height: 200px; +} +.dwv { + float: left; + margin: 5px; +} +.layerGroup { + width: 200px; + height: 200px; +} +.doc { + float: left; + margin: 5px; +} +.path { + border-bottom: 1px dashed #999; + text-decoration: none; +} +.key { + color: #999; +} +.separator { + clear: both; +} +.footer { + margin-top: 15px; + padding: 5px; + background-color: #ccc; +} diff --git a/yarn.lock b/yarn.lock index 456b9bfa74..5d658a39c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@7.12.11", "@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.10.4": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== @@ -143,10 +143,10 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== -"@babel/runtime@^7.12.0", "@babel/runtime@^7.5.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== +"@babel/runtime@^7.12.0", "@babel/runtime@^7.14.6", "@babel/runtime@^7.5.5": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" + integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== dependencies: regenerator-runtime "^0.13.4" @@ -183,44 +183,43 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@es-joy/jsdoccomment@^0.8.0-alpha.2": - version "0.8.0-alpha.2" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.8.0-alpha.2.tgz#78585147d8e6231270374dae528fe5b7b5587b5a" - integrity sha512-fjRY13Bh8sxDZkzO27U2R9L6xFqkh5fAbHuMGvGLXLfrTes8nTTMyOi6wIPt+CG0XPAxEUge8cDjhG+0aag6ew== +"@es-joy/jsdoccomment@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.12.0.tgz#47de05d86e9728ae3a5f1c57d6e9b63b07c6dc98" + integrity sha512-Gw4/j9v36IKY8ET+W0GoOzrRw17xjf21EIFFRL3zx21fF5MnqmeNpNi+PU/LKjqLpPb2Pw2XdlJbYM31VVo/PQ== dependencies: - comment-parser "^1.1.5" + comment-parser "1.2.4" esquery "^1.4.0" - jsdoc-type-pratt-parser "1.0.0-alpha.23" + jsdoc-type-pratt-parser "2.0.0" -"@eslint/eslintrc@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" - integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== +"@eslint/eslintrc@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.4.tgz#dfe0ff7ba270848d10c5add0715e04964c034b31" + integrity sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^12.1.0" + debug "^4.3.2" + espree "^9.0.0" + globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" - js-yaml "^3.13.1" + js-yaml "^4.1.0" minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@eslint/eslintrc@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.1.tgz#442763b88cecbe3ee0ec7ca6d6dd6168550cbf14" - integrity sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ== +"@humanwhocodes/config-array@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a" + integrity sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A== dependencies: - ajv "^6.12.4" + "@humanwhocodes/object-schema" "^1.2.0" debug "^4.1.1" - espree "^7.3.0" - globals "^12.1.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^3.13.1" minimatch "^3.0.4" - strip-json-comments "^3.1.1" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== "@iarna/cli@^1.2.0": version "1.2.0" @@ -246,15 +245,15 @@ resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== -"@types/cookie@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/cors@^2.8.8": - version "2.8.10" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" - integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== "@types/node@>=10.0.0": version "14.14.41" @@ -287,10 +286,10 @@ acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" + integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== agent-base@4, agent-base@^4.3.0: version "4.3.0" @@ -323,26 +322,6 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" - integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^8.0.1: - version "8.5.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.5.0.tgz#695528274bcb5afc865446aa275484049a18ae4b" - integrity sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - ansi-align@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" @@ -375,10 +354,10 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^2.2.1: version "2.2.1" @@ -417,10 +396,10 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -455,6 +434,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-each@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" @@ -482,12 +466,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async@^2.6.0, async@^2.6.2: +async@^2.6.0: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -539,21 +518,16 @@ base-64@^0.1.0: resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= -base64-arraybuffer@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" - integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= +base64-arraybuffer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c" + integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA== base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== -basic-auth@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884" - integrity sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ= - bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -761,10 +735,10 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@~4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^4.0.0, chalk@^4.1.2, chalk@~4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -774,20 +748,20 @@ chardet@^0.4.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I= -chokidar@^3.4.2: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chokidar@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.5.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.3.1" + fsevents "~2.3.2" chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.4: version "1.1.4" @@ -945,20 +919,20 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" - integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== +commander@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== commander@^2.11.0, commander@^2.19.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -comment-parser@1.1.5, comment-parser@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2" - integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA== +comment-parser@1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.2.4.tgz#489f3ee55dfd184a6e4bffb31baba284453cb760" + integrity sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw== component-emitter@~1.3.0: version "1.3.0" @@ -1074,11 +1048,6 @@ cors@~2.8.5: object-assign "^4" vary "^1" -corser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" - integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c= - create-error-class@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" @@ -1155,17 +1124,17 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@^3.1.0, debug@^3.1.1: +debug@^3.1.0: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@~4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@~4.3.1, debug@~4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" @@ -1313,16 +1282,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecstatic@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48" - integrity sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog== - dependencies: - he "^1.1.1" - mime "^1.6.0" - minimist "^1.1.0" - url-join "^2.0.5" - editor@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" @@ -1372,25 +1331,28 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -engine.io-parser@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e" - integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg== +engine.io-parser@~5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.1.tgz#6695fc0f1e6d76ad4a48300ff80db5f6b3654939" + integrity sha512-j4p3WwJrG2k92VISM0op7wiq60vO92MlF3CRGxhKHy9ywG1/Dkc72g0dXeDQ+//hrcDn8gqQzoEkdO9FN0d9AA== dependencies: - base64-arraybuffer "0.1.4" + base64-arraybuffer "~1.0.1" -engine.io@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b" - integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w== +engine.io@~6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.0.1.tgz#4a37754c6067415e9bfbcc82e49e572437354615" + integrity sha512-Y53UaciUh2Rmx5MiogtMxOQcfh7pnemday+Bb4QDg0Wjmnvo/VTvuEyNGQgYmh8L7VOe8Je1QuiqjLNDelMqLA== dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "2.0.0" cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~4.0.0" - ws "~7.4.2" + engine.io-parser "~5.0.0" + ws "~8.2.3" enquirer@^2.3.5: version "2.3.6" @@ -1504,119 +1466,76 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-jsdoc@~35.1.2: - version "35.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-35.1.2.tgz#dbd447f61e9baa6c369eb85d6cd8c2df0e1f63c9" - integrity sha512-IPChTbaL9rWe6DCinKOpUdqsBV7r2dKEId1nweSKsjJqZp1VAQyzQJ5N6ogji2AWmrPU1jdjfHA5HIG2RaiRBA== +eslint-plugin-jsdoc@~37.0.3: + version "37.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-37.0.3.tgz#42ddd0393c166c2724a7fdee808b94ab1d9dfb00" + integrity sha512-Qg/gIZAfcrM4Qu/JzcnxPGD45Je6wPLFzMZQboeqit/CL4aY6wuzBTkgUMiWXfw/PaPl+sb0GF1XdBlV23ReDA== dependencies: - "@es-joy/jsdoccomment" "^0.8.0-alpha.2" - comment-parser "1.1.5" - debug "^4.3.1" + "@es-joy/jsdoccomment" "0.12.0" + comment-parser "1.2.4" + debug "^4.3.2" esquery "^1.4.0" - jsdoc-type-pratt-parser "^1.0.2" + jsdoc-type-pratt-parser "^2.0.0" lodash "^4.17.21" regextras "^0.8.0" semver "^7.3.5" spdx-expression-parse "^3.0.1" -eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== +eslint-scope@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.0.tgz#c1f6ea30ac583031f203d65c73e723b01298f153" + integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== dependencies: esrecurse "^4.3.0" - estraverse "^4.1.1" + estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + eslint-visitor-keys "^2.0.0" eslint-visitor-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.0.0: - version "7.24.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.24.0.tgz#2e44fa62d93892bfdb100521f17345ba54b8513a" - integrity sha512-k9gaHeHiFmGCDQ2rEfvULlSLruz6tgfA8DEn+rY9/oYPFFTlz55mM/Q/Rij1b2Y42jwZiK3lXvNTw6w6TXzcKQ== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.0" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.0.1" - doctrine "^3.0.0" - enquirer "^2.3.5" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash "^4.17.21" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.4" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" +eslint-visitor-keys@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" + integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== -eslint@~7.27.0: - version "7.27.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.27.0.tgz#665a1506d8f95655c9274d84bd78f7166b07e9c7" - integrity sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA== +eslint@^8.0.1, eslint@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.3.0.tgz#a3c2409507403c1c7f6c42926111d6cbefbc3e85" + integrity sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.1" + "@eslint/eslintrc" "^1.0.4" + "@humanwhocodes/config-array" "^0.6.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + eslint-scope "^7.1.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.1.0" + espree "^9.1.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" + glob-parent "^6.0.1" globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" @@ -1624,22 +1543,21 @@ eslint@~7.27.0: natural-compare "^1.4.0" optionator "^0.9.1" progress "^2.0.0" - regexpp "^3.1.0" + regexpp "^3.2.0" semver "^7.2.1" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.0.0, espree@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.1.0.tgz#ba9d3c9b34eeae205724124e31de4543d59fbf74" + integrity sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ== dependencies: - acorn "^7.4.0" + acorn "^8.6.0" acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + eslint-visitor-keys "^3.1.0" esprima@^4.0.0: version "4.0.1" @@ -1660,11 +1578,6 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - estraverse@^5.1.0, estraverse@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" @@ -1949,7 +1862,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-minipass@^1.2.5: +fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== @@ -1980,10 +1893,10 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.1.tgz#b209ab14c61012636c8863507edf7fb68cc54e9f" - integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== function-bind@^1.1.1: version "1.1.1" @@ -2121,17 +2034,24 @@ github-release-notes@0.17.2: require-yaml "0.0.1" valid-url "^1.0.9" -glob-parent@^5.0.0, glob-parent@~5.1.0: +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1, glob@~7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.1.1, glob@~7.1.6: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2183,17 +2103,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" - -globals@^13.6.0: - version "13.8.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" - integrity sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q== +globals@^13.6.0, globals@^13.9.0: + version "13.10.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.10.0.tgz#60ba56c3ac2ca845cfbf4faeca727ad9dd204676" + integrity sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g== dependencies: type-fest "^0.20.2" @@ -2233,28 +2146,28 @@ got@^6.7.1: unzip-response "^2.0.1" url-parse-lax "^1.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== grunt-cli@^1.4.2, grunt-cli@~1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.4.2.tgz#8a83dcc89ebd92d278ccd6014105011cffc71165" - integrity sha512-wsu6BZh7KCnfeaSkDrKIAvOlqGKxNRTZjc8xfZlvxCByQIqUfZ31kh5uHpPnhQ4NdVgvaWaVxa1LUbVU80nACw== + version "1.4.3" + resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.4.3.tgz#22c9f1a3d2780bf9b0d206e832e40f8f499175ff" + integrity sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ== dependencies: - grunt-known-options "~1.1.1" + grunt-known-options "~2.0.0" interpret "~1.1.0" liftup "~3.0.1" nopt "~4.0.1" v8flags "~3.2.0" -grunt-contrib-concat@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz#61509863084e871d7e86de48c015259ed97745bd" - integrity sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0= +grunt-contrib-concat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/grunt-contrib-concat/-/grunt-contrib-concat-2.0.0.tgz#3af65e4663186abce6052a6bed345b53c0551090" + integrity sha512-/cfWwsGiprVTOl7c2bZwMdQ8hIf3e1f4szm1i7qhY9hOnR/X2KL+Xe7dynNweTYHa6aWPZx2B5GPsUpxAXNCaA== dependencies: - chalk "^1.0.0" + chalk "^4.1.2" source-map "^0.5.3" grunt-contrib-copy@^1.0.0: @@ -2285,13 +2198,13 @@ grunt-contrib-watch@^1.1.0: lodash "^4.17.10" tiny-lr "^1.1.1" -grunt-eslint@^23.0.0: - version "23.0.0" - resolved "https://registry.yarnpkg.com/grunt-eslint/-/grunt-eslint-23.0.0.tgz#47c804613c59646b2cfa402eb8d301e55b206bcf" - integrity sha512-QqHSAiGF08EVD7YlD4OSRWuLRaDvpsRdTptwy9WaxUXE+03mCLVA/lEaR6SHWehF7oUwIqCEjaNONeeeWlB4LQ== +grunt-eslint@^24.0.0: + version "24.0.0" + resolved "https://registry.yarnpkg.com/grunt-eslint/-/grunt-eslint-24.0.0.tgz#ad14e1cd9c869e0365e2bf4a97241c0c8844c7cc" + integrity sha512-WpTeBBFweyhMuPjGwRSQV9JFJ+EczIdlsc7Dd/1g78QVI1aZsk4g/H3e+3S5HEwsS1RKL2YZIrGj8hMLlBfN8w== dependencies: - chalk "^4.0.0" - eslint "^7.0.0" + chalk "^4.1.2" + eslint "^8.0.1" grunt-karma@^4.0.2: version "4.0.2" @@ -2300,11 +2213,6 @@ grunt-karma@^4.0.2: dependencies: lodash "^4.17.10" -grunt-known-options@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.1.tgz#6cc088107bd0219dc5d3e57d91923f469059804d" - integrity sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ== - grunt-known-options@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-2.0.0.tgz#cac641e897f9a0a680b8c9839803d35f3325103c" @@ -2416,11 +2324,6 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -he@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -2472,7 +2375,7 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy@^1.18.0, http-proxy@^1.18.1: +http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -2481,22 +2384,6 @@ http-proxy@^1.18.0, http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-server@^0.12.3: - version "0.12.3" - resolved "https://registry.yarnpkg.com/http-server/-/http-server-0.12.3.tgz#ba0471d0ecc425886616cb35c4faf279140a0d37" - integrity sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA== - dependencies: - basic-auth "^1.0.3" - colors "^1.4.0" - corser "^2.0.1" - ecstatic "^3.3.2" - http-proxy "^1.18.0" - minimist "^1.2.5" - opener "^1.5.1" - portfinder "^1.0.25" - secure-compare "3.0.1" - union "~0.5.0" - http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -2521,12 +2408,12 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -i18next-browser-languagedetector@~6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.0.1.tgz#83654bc87302be2a6a5a75146ffea97b4ca268cf" - integrity sha512-3H+OsNQn3FciomUU0d4zPFHsvJv4X66lBelXk9hnIDYDsveIgT7dWZ3/VvcSlpKk9lvCK770blRZ/CwHMXZqWw== +i18next-browser-languagedetector@~6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.2.tgz#68565a28b929cbc98ab6a56826ef2faf0e927ff8" + integrity sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g== dependencies: - "@babel/runtime" "^7.5.5" + "@babel/runtime" "^7.14.6" i18next-xhr-backend@~3.2.2: version "3.2.2" @@ -2535,10 +2422,10 @@ i18next-xhr-backend@~3.2.2: dependencies: "@babel/runtime" "^7.5.5" -i18next@~20.1.0: - version "20.1.0" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.1.0.tgz#397dce9ad230ea822f487dc62d6128df5b678456" - integrity sha512-sV+ZwTM4Ik4d6wKdwNS/ocKmvXi6DFA/YHMgdQX3i4L5993jnbo1/j1pK/c4+zBOjexer4dt+c5JHsFj4CUoXQ== +i18next@~20.6.0: + version "20.6.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345" + integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A== dependencies: "@babel/runtime" "^7.12.0" @@ -2761,10 +2648,10 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -2873,10 +2760,10 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isbinaryfile@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" - integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== +isbinaryfile@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf" + integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w== isexe@^2.0.0: version "2.0.0" @@ -2950,17 +2837,19 @@ js-beautify@^1.7.4: mkdirp "^1.0.4" nopt "^5.0.0" -js-reporters@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-2.0.0.tgz#62ad6a512f1740d3ab4686b0059dd9f57cc0708b" - integrity sha512-VJd/86niT7GzsaVc+Yxrs8QPrYl1orzv8bYZPuSOtxU6rk/pv8aOXTcIa7HaANvtvdLMTsZspAiucNQ6T2QFHw== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@, js-yaml@^3.13.1, js-yaml@~3.14.0: +js-yaml@, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@~3.14.0: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -2980,15 +2869,10 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= -jsdoc-type-pratt-parser@1.0.0-alpha.23: - version "1.0.0-alpha.23" - resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-1.0.0-alpha.23.tgz#01c232d92b99b7e7ef52235ab8c9115137426639" - integrity sha512-COtimMd97eo5W0h6R9ISFj9ufg/9EiAzVAeQpKBJ1xJs/x8znWE155HGBDR2rwOuZsCes1gBXGmFVfvRZxGrhg== - -jsdoc-type-pratt-parser@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-1.0.4.tgz#5750d2d32ffb001866537d3baaedea7cf84c7036" - integrity sha512-jzmW9gokeq9+bHPDR1nCeidMyFUikdZlbOhKzh9+/nJqB75XhpNKec1/UuxW5c4+O+Pi31Gc/dCboyfSm/pSpQ== +jsdoc-type-pratt-parser@2.0.0, jsdoc-type-pratt-parser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.0.0.tgz#ec739a0868922515fcb179852e990e89b52b9044" + integrity sha512-sUuj2j48wxrEpbFjDp1sAesAxPiLT+z0SWVmMafyIINs6Lj5gIPKh3VrkBZu4E/Dv+wHpOot0m6H8zlHQjwqeQ== jsdoc@^3.6.7: version "3.6.7" @@ -3030,11 +2914,6 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -3086,10 +2965,10 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jszip@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" - integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== +jszip@~3.7.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -3120,33 +2999,33 @@ karma-qunit@^4.1.2: resolved "https://registry.yarnpkg.com/karma-qunit/-/karma-qunit-4.1.2.tgz#fa27697470e749fb4bd63b1b5e487b6165ec9ec8" integrity sha512-taTPqBeHCOlkeKTSzQgIKzAUb79vw3rfbCph+xwwh63tyGjNtljwx91VArhIM9DzIIR3gB9G214wQg+oXI9ycw== -karma@^6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.2.tgz#24b62fbae3e8b5218cc32a0dac49ad08a541e76d" - integrity sha512-fo4Wt0S99/8vylZMxNj4cBFyOBBnC1bewZ0QOlePij/2SZVWxqbyLeIddY13q6URa2EpLRW8ixvFRUMjkmo1bw== +karma@^6.3.4: + version "6.3.9" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.9.tgz#cc309607f0fcdb58a88643184f3e4ba8bff26751" + integrity sha512-E/MqdLM9uVIhfuyVnrhlGBu4miafBdXEAEqCmwdEMh3n17C7UWC/8Kvm3AYKr91gc7scutekZ0xv6rxRaUCtnw== dependencies: body-parser "^1.19.0" braces "^3.0.2" - chokidar "^3.4.2" + chokidar "^3.5.1" colors "^1.4.0" connect "^3.7.0" di "^0.0.1" dom-serialize "^2.2.1" - glob "^7.1.6" - graceful-fs "^4.2.4" + glob "^7.1.7" + graceful-fs "^4.2.6" http-proxy "^1.18.1" - isbinaryfile "^4.0.6" - lodash "^4.17.19" - log4js "^6.2.1" - mime "^2.4.5" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.3.0" + mime "^2.5.2" minimatch "^3.0.4" qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^3.1.0" + socket.io "^4.2.0" source-map "^0.6.1" - tmp "0.2.1" - ua-parser-js "^0.7.23" + tmp "^0.2.1" + ua-parser-js "^0.7.30" yargs "^16.1.1" kind-of@^6.0.2: @@ -3161,10 +3040,10 @@ klaw@^3.0.0: dependencies: graceful-fs "^4.1.9" -konva@~7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/konva/-/konva-7.2.2.tgz#e46598850d864b0dca68c82013f1e436b1eef55d" - integrity sha512-3vtCLFV8S9ooMEC/ELMoHJLWK3YtVUIx44FZFdM1FUBZi88qF4ctdzeII5hfT9Lx6ulVlhC0a6cRdP0uRWjMmw== +konva@~8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/konva/-/konva-8.3.0.tgz#538452fedc0d0e135415f5f53787f779e8530027" + integrity sha512-V83lFjf1xPJp7SzWKktOgRFPYa8SOHKVfgru3jjdMjKTGNpCGrIh3CuJeKC/tJ5Fg0P3lOpPCR30gDbDsATmDA== latest-version@^3.0.0: version "3.1.0" @@ -3430,11 +3309,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - lodash.union@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" @@ -3450,7 +3324,7 @@ lodash.without@~4.4.0: resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw= -lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.19, lodash@~4.17.21: +lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.19, lodash@~4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3462,7 +3336,7 @@ log-symbols@^2.1.0: dependencies: chalk "^2.0.1" -log4js@^6.2.1: +log4js@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== @@ -3621,15 +3495,10 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.44.0" -mime@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.4.5: - version "2.5.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.0.tgz#2b4af934401779806ee98026bb42e8c1ae1876b1" - integrity sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag== +mime@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== mimic-fn@^1.0.0: version "1.2.0" @@ -3643,12 +3512,12 @@ mimic-fn@^1.0.0: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: +minipass@^2.3.5, minipass@^2.6.0, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -3656,7 +3525,7 @@ minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minizlib@^1.2.1: +minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== @@ -3679,7 +3548,7 @@ mississippi@^3.0.0: stream-each "^1.1.0" through2 "^2.0.0" -mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.0: +mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -3767,10 +3636,10 @@ node-gyp@^5.0.2, node-gyp@^5.1.0: tar "^4.4.12" which "^1.3.1" -node-watch@0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.1.tgz#0caaa6a6833b0d533487f953c52a6c787769ba7c" - integrity sha512-UWblPYuZYrkCQCW5PxAwYSxaELNBLUckrTBBk8xr1/bUgyOkYYTsUcV4e3ytcazFEOyiRyiUrsG37pu6I0I05g== +node-watch@0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.2.tgz#545f057da8500487eb8287adcb4cb5a7338d7e21" + integrity sha512-g53VjSARRv1JdST0LZRIg8RiuLr1TaBbVPsVvxh0/0Ymvi0xYUjDuoqQQAWtHJQUXhiShowPT/aXKNeHBcyQsw== nopt@^4.0.1, nopt@^4.0.3, nopt@~4.0.1: version "4.0.3" @@ -4364,9 +4233,9 @@ path-key@^3.1.0: integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-root-regex@^0.1.0: version "0.1.2" @@ -4417,15 +4286,6 @@ platform@^1.3.3: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== -portfinder@^1.0.25: - version "1.0.28" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" - integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== - dependencies: - async "^2.6.2" - debug "^3.1.1" - mkdirp "^0.5.5" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -4565,14 +4425,13 @@ query-string@^6.8.2: strict-uri-encode "^2.0.0" qunit@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.15.0.tgz#8ba3a3c5d13369ab1740337680600a98a7f8b591" - integrity sha512-9ZoOILeyRZzrdvy2m7M4S76bneGD75Bh4B2aot3uKRKZuoEvA9gevvzU339L805Ys0AN2C7cnAV9nIBD5t72IQ== + version "2.17.2" + resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.17.2.tgz#5cb278e131d931f25c109a0fdb0518be7754c25a" + integrity sha512-17isVvuOmALzsPjiV7wFg/6O5vJYXBrQZPwocfQSSh0I/rXvfX7bKMFJ4GMVW3U4P8r2mBeUy8EAngti4QD2Vw== dependencies: - commander "7.1.0" - js-reporters "2.0.0" - node-watch "0.7.1" - tiny-glob "0.2.8" + commander "7.2.0" + node-watch "0.7.2" + tiny-glob "0.2.9" qw@~1.0.1: version "1.0.1" @@ -4718,10 +4577,10 @@ readdir-scoped-modules@^1.0.0, readdir-scoped-modules@^1.1.0: graceful-fs "^4.1.2" once "^1.3.0" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" @@ -4747,10 +4606,10 @@ regex-match-all@^1.0.2: resolved "https://registry.yarnpkg.com/regex-match-all/-/regex-match-all-1.0.2.tgz#f9cf3fb49694b417314d18336d204091427b4af3" integrity sha1-+c8/tJaUtBcxTRgzbSBAkUJ7SvM= -regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== regextras@^0.8.0: version "0.8.0" @@ -4808,11 +4667,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - require-from-url@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/require-from-url/-/require-from-url-3.1.3.tgz#e732912b557163229e47ed4c4db0a9631d7196d5" @@ -4944,7 +4798,7 @@ rx-lite@*, rx-lite@^4.0.8: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -4964,11 +4818,6 @@ safe-json-parse@~1.0.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -secure-compare@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3" - integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM= - semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -5049,15 +4898,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - slide@^1.1.6, slide@~1.1.3, slide@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -5068,12 +4908,12 @@ smart-buffer@^4.1.0: resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== -socket.io-adapter@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527" - integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== +socket.io-adapter@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.2.tgz#039cd7c71a52abad984a6d57da2c0b7ecdd3c289" + integrity sha512-PBZpxUPYjmoogY0aoaTmo1643JelsaS1CiAwNjRVdrI0X9Seuc19Y2Wife8k88avW6haG8cznvwbubAZwH4Mtg== -socket.io-parser@~4.0.3: +socket.io-parser@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== @@ -5082,20 +4922,17 @@ socket.io-parser@~4.0.3: component-emitter "~1.3.0" debug "~4.3.1" -socket.io@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a" - integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw== +socket.io@^4.2.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.3.2.tgz#85ae0cf5cf18acbce648ac9f48aba66df8cea6bf" + integrity sha512-6S5tV4jcY6dbZ/lLzD6EkvNWI3s81JO6ABP/EpvOlK1NPOcIj3AS4khi6xXw6JlZCASq82HQV4SapfmVMMl2dg== dependencies: - "@types/cookie" "^0.4.0" - "@types/cors" "^2.8.8" - "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "~2.0.0" - debug "~4.3.1" - engine.io "~4.1.0" - socket.io-adapter "~2.1.0" - socket.io-parser "~4.0.3" + debug "~4.3.2" + engine.io "~6.0.0" + socket.io-adapter "~2.3.2" + socket.io-parser "~4.0.4" socks-proxy-agent@^4.0.0: version "4.0.2" @@ -5340,12 +5177,12 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-bom@^3.0.0: version "3.0.0" @@ -5386,45 +5223,23 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -table@^6.0.4: - version "6.0.7" - resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" - integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== - dependencies: - ajv "^7.0.2" - lodash "^4.17.20" - slice-ansi "^4.0.0" - string-width "^4.2.0" - -table@^6.0.9: - version "6.7.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" - integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== - dependencies: - ajv "^8.0.1" - lodash.clonedeep "^4.5.0" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.0" - strip-ansi "^6.0.0" - taffydb@2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" integrity sha1-fLy2S1oUG2ou/CxdLGe04VCyomg= tar@^4.4.10, tar@^4.4.12, tar@^4.4.13: - version "4.4.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" - integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + version "4.4.19" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" term-size@^1.2.0: version "1.2.0" @@ -5456,10 +5271,10 @@ timed-out@^4.0.0: resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= -tiny-glob@0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.8.tgz#b2792c396cc62db891ffa161fe8b33e76123e531" - integrity sha512-vkQP7qOslq63XRX9kMswlby99kyO5OvKptw7AMwBVMjXEI7Tb61eoI5DydyEMOseyGS5anDN1VPoVxEvH01q8w== +tiny-glob@0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== dependencies: globalyzer "0.1.0" globrex "^0.1.2" @@ -5481,13 +5296,6 @@ tiny-relative-date@^1.3.0: resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== -tmp@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -5495,6 +5303,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -5544,11 +5359,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - type-is@~1.6.17: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -5562,10 +5372,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -ua-parser-js@^0.7.23: - version "0.7.28" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" - integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +ua-parser-js@^0.7.30: + version "0.7.31" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" + integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -5605,13 +5415,6 @@ underscore@~1.13.1: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== -union@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075" - integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA== - dependencies: - qs "^6.4.0" - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -5676,11 +5479,6 @@ uri-path@^1.0.0: resolved "https://registry.yarnpkg.com/uri-path/-/uri-path-1.0.0.tgz#9747f018358933c31de0fccfd82d138e67262e32" integrity sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI= -url-join@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728" - integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg= - url-parse-lax@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" @@ -5877,10 +5675,10 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.3: imurmurhash "^0.1.4" signal-exit "^3.0.2" -ws@~7.4.2: - version "7.4.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== xdg-basedir@^3.0.0: version "3.0.0" @@ -5917,7 +5715,7 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==