// Main DWV namespace. var dwv = dwv || {}; var Kinetic = Kinetic || {}; /** * Main application class. * @class App * @namespace dwv * @constructor */ dwv.App = function() { // Local object var self = this; // Image var image = null; // View var view = null; // Original image var originalImage = null; // Image data array var imageData = null; // Image data width var dataWidth = 0; // Image data height var dataHeight = 0; // display window scale var windowScale = 1; // Image layer var imageLayer = null; // Draw layers var drawLayers = []; // Draw stage var drawStage = null; // flag to know if the info layer is listening on the image. var isInfoLayerListening = false; // Tool box var toolBox = new dwv.tool.ToolBox(this); // UndoStack var undoStack = new dwv.tool.UndoStack(); /** * Get the version of the application. * @method getVersion * @return {String} The version of the application. */ this.getVersion = function() { return "v0.8.0beta"; }; /** * Get the image. * @method getImage * @return {Image} The associated image. */ this.getImage = function() { return image; }; /** * Get the view. * @method getView * @return {Image} The associated view. */ this.getView = function() { return view; }; /** * Set the view. * @method setImage * @param {Image} img The associated image. */ this.setImage = function(img) { image = img; view.setImage(img); }; /** * Restore the original image. * @method restoreOriginalImage */ this.restoreOriginalImage = function() { image = originalImage; view.setImage(originalImage); }; /** * Get the image data array. * @method getImageData * @return {Array} The image data array. */ this.getImageData = function() { return imageData; }; /** * Get the tool box. * @method getToolBox * @return {Object} The associated toolbox. */ this.getToolBox = function() { return toolBox; }; /** * Get the image layer. * @method getImageLayer * @return {Object} The image layer. */ this.getImageLayer = function() { return imageLayer; }; /** * Get the draw layer. * @method getDrawLayer * @return {Object} The draw layer. */ this.getDrawLayer = function() { return drawLayers[view.getCurrentPosition().k]; }; /** * Get the draw stage. * @method getDrawStage * @return {Object} The draw layer. */ this.getDrawStage = function() { return drawStage; }; /** * Get the undo stack. * @method getUndoStack * @return {Object} The undo stack. */ this.getUndoStack = function() { return undoStack; }; /** * Initialise the HTML for the application. * @method init */ this.init = function(){ // align layers when the window is resized window.onresize = this.resize; // possible load from URL if( typeof skipLoadUrl === "undefined" ) { var inputUrls = dwv.html.getUriParam(); if( inputUrls && inputUrls.length > 0 ) { this.loadURL(inputUrls); } } else{ console.log("Not loading url from adress since skipLoadUrl is defined."); } }; /** * Reset the application. * @method reset */ this.reset = function() { image = null; view = null; undoStack = new dwv.tool.UndoStack(); dwv.gui.cleaUndoHtml(); }; /** * Reset the layout of the application. * @method resetLayout */ this.resetLayout = function () { if ( imageLayer ) { imageLayer.resetLayout(windowScale); imageLayer.draw(); } if ( drawStage ) { drawStage.offset( {'x': 0, 'y': 0} ); drawStage.scale( {'x': windowScale, 'y': windowScale} ); drawStage.draw(); } }; /** * Handle key down event. * - CRTL-Z: undo * - CRTL-Y: redo * Default behavior. Usually used in tools. * @method onKeydown * @param {Object} event The key down event. */ this.onKeydown = function(event) { if( event.keyCode === 90 && event.ctrlKey ) // ctrl-z { undoStack.undo(); } else if( event.keyCode === 89 && event.ctrlKey ) // ctrl-y { undoStack.redo(); } }; /** * Handle change files event. * @method onChangeFiles * @param {Object} event The event fired when changing the file field. */ this.onChangeFiles = function(event) { this.loadFiles(event.target.files); }; /** * Load a list of files. * @method loadFiles * @param {Array} files The list of files to load. */ this.loadFiles = function(files) { // clear variables this.reset(); // create IO var fileIO = new dwv.io.File(); fileIO.onload = function (data) { var isFirst = true; if( image ) { image.appendSlice( data.view.getImage() ); isFirst = false; } postLoadInit(data); if( drawStage ) { // create slice draw layer var drawLayer = new Kinetic.Layer({ listening: false, hitGraphEnabled: false, visible: isFirst }); // add to layers array drawLayers.push(drawLayer); // add the layer to the stage drawStage.add(drawLayer); } }; fileIO.onerror = function(error){ handleError(error); }; // main load (asynchronous) fileIO.load(files); }; /** * Handle change url event. * @method onChangeURL * @param {Object} event The event fired when changing the url field. */ this.onChangeURL = function(event) { this.loadURL([event.target.value]); }; /** * Load a list of URLs. * @method loadURL * @param {Array} urls The list of urls to load. */ this.loadURL = function(urls) { // clear variables this.reset(); // create IO var urlIO = new dwv.io.Url(); urlIO.onload = function (data) { var isFirst = true; if( image ) { image.appendSlice( data.view.getImage() ); isFirst = false; } postLoadInit(data); if( drawStage ) { // create slice draw layer var drawLayer = new Kinetic.Layer({ listening: false, hitGraphEnabled: false, visible: isFirst }); // add to layers array drawLayers.push(drawLayer); // add the layer to the stage drawStage.add(drawLayer); } }; urlIO.onerror = function(error){ handleError(error); }; // main load (asynchronous) urlIO.load(urls); }; /** * Handle window/level change. * @method onWLChange * @param {Object} event The event fired when changing the window/level. */ this.onWLChange = function (/*event*/) { generateAndDrawImage(); }; /** * Handle color map change. * @method onColorChange * @param {Object} event The event fired when changing the color map. */ this.onColorChange = function (/*event*/) { generateAndDrawImage(); }; /** * Handle slice change. * @method onSliceChange * @param {Object} event The event fired when changing the slice. */ this.onSliceChange = function (/*event*/) { generateAndDrawImage(); if ( drawStage ) { // hide all draw layers for ( var i = 0; i < drawLayers.length; ++i ) { drawLayers[i].visible( false ); } // show current draw layer var currentLayer = drawLayers[view.getCurrentPosition().k]; currentLayer.visible( true ); currentLayer.draw(); } }; /** * Resize the display window. To be called once the image is loaded. * @method resize */ this.resize = function() { // previous width var oldWidth = parseInt(windowScale*dataWidth, 10); // find new best fit var size = dwv.gui.getWindowSize(); windowScale = Math.min( (size.width / dataWidth), (size.height / dataHeight) ); // new sizes var newWidth = parseInt(windowScale*dataWidth, 10); var newHeight = parseInt(windowScale*dataHeight, 10); // ratio previous/new to add to zoom var mul = newWidth / oldWidth; // resize container $("#layerContainer").width(newWidth); $("#layerContainer").height(newHeight + 1); // +1 to be sure... // resize image layer if( imageLayer ) { var iZoomX = imageLayer.getZoom().x * mul; var iZoomY = imageLayer.getZoom().y * mul; imageLayer.setWidth(newWidth); imageLayer.setHeight(newHeight); imageLayer.zoom(iZoomX, iZoomY, 0, 0); imageLayer.draw(); } // resize draw stage if( drawStage ) { // resize div $("#drawDiv").width(newWidth); $("#drawDiv").height(newHeight); // resize stage var stageZomX = drawStage.scale().x * mul; var stageZoomY = drawStage.scale().y * mul; drawStage.setWidth(newWidth); drawStage.setHeight(newHeight); drawStage.scale( {x: stageZomX, y: stageZoomY} ); drawStage.draw(); } }; /** * Toggle the display of the info layer. * @method toggleInfoLayerDisplay */ this.toggleInfoLayerDisplay = function() { // toggle html dwv.html.toggleDisplay('infoLayer'); // toggle listeners if( isInfoLayerListening ) { removeImageInfoListeners(); } else { addImageInfoListeners(); } }; /** * Init the Window/Level display */ this.initWLDisplay = function() { // set window/level var keys = Object.keys(dwv.tool.presets); dwv.tool.updateWindowingData( dwv.tool.presets[keys[0]].center, dwv.tool.presets[keys[0]].width ); // default position dwv.tool.updatePostionValue(0,0); }; /** * Add layer mouse and touch listeners. * @method addLayerListeners */ this.addLayerListeners = function(layer) { // allow pointer events layer.setAttribute("style", "pointer-events: auto;"); // mouse listeners layer.addEventListener("mousedown", eventHandler); layer.addEventListener("mousemove", eventHandler); layer.addEventListener("mouseup", eventHandler); layer.addEventListener("mouseout", eventHandler); layer.addEventListener("mousewheel", eventHandler); layer.addEventListener("DOMMouseScroll", eventHandler); layer.addEventListener("dblclick", eventHandler); // touch listeners layer.addEventListener("touchstart", eventHandler); layer.addEventListener("touchmove", eventHandler); layer.addEventListener("touchend", eventHandler); }; /** * Remove layer mouse and touch listeners. * @method removeLayerListeners */ this.removeLayerListeners = function(layer) { // disable pointer events layer.setAttribute("style", "pointer-events: none;"); // mouse listeners layer.removeEventListener("mousedown", eventHandler); layer.removeEventListener("mousemove", eventHandler); layer.removeEventListener("mouseup", eventHandler); layer.removeEventListener("mouseout", eventHandler); layer.removeEventListener("mousewheel", eventHandler); layer.removeEventListener("DOMMouseScroll", eventHandler); layer.removeEventListener("dblclick", eventHandler); // touch listeners layer.removeEventListener("touchstart", eventHandler); layer.removeEventListener("touchmove", eventHandler); layer.removeEventListener("touchend", eventHandler); }; /** * Render the current image. * @method render */ this.render = function () { generateAndDrawImage(); }; // Private Methods ------------------------------------------- /** * Generate the image data and draw it. * @method generateAndDrawImage */ function generateAndDrawImage() { // generate image data from DICOM view.generateImageData(imageData); // set the image data of the layer imageLayer.setImageData(imageData); // draw the image imageLayer.draw(); } /** * Add image listeners. * @method addImageInfoListeners * @private */ function addImageInfoListeners() { view.addEventListener("wlchange", dwv.info.updateWindowingDiv); view.addEventListener("wlchange", dwv.info.updateMiniColorMap); view.addEventListener("wlchange", dwv.info.updatePlotMarkings); view.addEventListener("colorchange", dwv.info.updateMiniColorMap); view.addEventListener("positionchange", dwv.info.updatePositionDiv); isInfoLayerListening = true; } /** * Remove image listeners. * @method removeImageInfoListeners * @private */ function removeImageInfoListeners() { view.removeEventListener("wlchange", dwv.info.updateWindowingDiv); view.removeEventListener("wlchange", dwv.info.updateMiniColorMap); view.removeEventListener("wlchange", dwv.info.updatePlotMarkings); view.removeEventListener("colorchange", dwv.info.updateMiniColorMap); view.removeEventListener("positionchange", dwv.info.updatePositionDiv); isInfoLayerListening = false; } /** * General-purpose event handler. This function just determines the mouse * position relative to the canvas element. It then passes it to the current tool. * @method eventHandler * @private * @param {Object} event The event to handle. */ function eventHandler(event) { // 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.preventDefault(); // event offset(s) offsets = dwv.html.getEventOffset(event); // should have at least one offset event._xs = offsets[0].x; event._ys = offsets[0].y; position = self.getImageLayer().displayToIndex( 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 = self.getImageLayer().displayToIndex( 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 === "mousewheel" || event.type === "dblclick" || event.type === "DOMMouseScroll" ) { offsets = dwv.html.getEventOffset(event); event._xs = offsets[0].x; event._ys = offsets[0].y; position = self.getImageLayer().displayToIndex( 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 tool. if( handled ) { var func = self.getToolBox().getSelectedTool()[event.type]; if( func ) { func(event); } } } /** * Handle an error: display it to the user. * @method handleError * @private * @param {Object} error The error to handle. */ function handleError(error) { // alert window if( error.name && error.message) { alert(error.name+": "+error.message+"."); } else { alert("Error: "+error+"."); } // log if( error.stack ) { console.error(error.stack); } } /** * Create the application layers. * @method createLayers * @private * @param {Number} dataWidth The width of the input data. * @param {Number} dataHeight The height of the input data. */ function createLayers(dataWidth, dataHeight) { // image layer imageLayer = new dwv.html.Layer("imageLayer"); imageLayer.initialise(dataWidth, dataHeight); imageLayer.fillContext(); imageLayer.setStyleDisplay(true); // draw layer if( document.getElementById("drawDiv") !== null) { // create stage drawStage = new Kinetic.Stage({ container: 'drawDiv', width: dataWidth, height: dataHeight, listening: false }); } // resize app self.resetLayout(); self.resize(); } /** * Create the DICOM tags table. To be called once the DICOM has been parsed. * @method createTagsTable * @private * @param {Object} dataInfo The data information. */ function createTagsTable(dataInfo) { // HTML node var node = document.getElementById("tags"); if( node === null ) { return; } // tag list table (without the pixel data) if(dataInfo.PixelData) { dataInfo.PixelData.value = "..."; } // remove possible previous while (node.hasChildNodes()) { node.removeChild(node.firstChild); } // tags HTML table var table = dwv.html.toTable(dataInfo); table.id = "tagsTable"; table.className = "tagsList table-stripe"; table.setAttribute("data-role", "table"); table.setAttribute("data-mode", "columntoggle"); // search form node.appendChild(dwv.html.getHtmlSearchForm(table)); // tags table node.appendChild(table); } /** * Post load application initialisation. To be called once the DICOM has been parsed. * @method postLoadInit * @private * @param {Object} data The data to display. */ function postLoadInit(data) { // only initialise the first time if( view ) { return; } // get the view from the loaded data view = data.view; // create the DICOM tags table createTagsTable(data.info); // store image originalImage = view.getImage(); image = originalImage; // layout dataWidth = image.getSize().getNumberOfColumns(); dataHeight = image.getSize().getNumberOfRows(); createLayers(dataWidth, dataHeight); // get the image data from the image layer imageData = imageLayer.getContext().createImageData( dataWidth, dataHeight); // mouse and touch listeners self.addLayerListeners( imageLayer.getCanvas() ); // keydown listener window.addEventListener("keydown", eventHandler, true); // image listeners view.addEventListener("wlchange", self.onWLChange); view.addEventListener("colorchange", self.onColorChange); view.addEventListener("slicechange", self.onSliceChange); // info layer if(document.getElementById("infoLayer")){ dwv.info.createWindowingDiv(); dwv.info.createPositionDiv(); dwv.info.createMiniColorMap(); dwv.info.createPlot(); addImageInfoListeners(); } // initialise the toolbox toolBox.init(); toolBox.display(true); // init W/L display self.initWLDisplay(); } };